> 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/como-fazer/migrar-codigo-existente.md).

# Migrar código existente

Como pegar um fluxo que já existe no seu sistema (jobs, filas, tabela de controle) e transformá-lo em uma function skail, sem reescrever o sistema.

O guia tem quatro passos e mostra o código antes e depois.

## Pré-requisitos

O runtime configurado no projeto (ou em um worker novo ao lado do sistema): [Como configurar o runtime](/opere/configurar-e-hospedar/configurar-o-runtime.md). Acesso ao código do fluxo que vai migrar e ao banco que ele usa.

## 1. Escolha um fluxo, não o sistema

O candidato certo tem várias etapas que gravam, cobram ou enviam algo, espera por algo externo, e dá trabalho quando quebra:

* reservar estoque;
* esperar o pagamento;
* gerar etiqueta;
* esperar a transportadora;
* atualizar status.

Hoje isso está espalhado em um job que roda a cada cinco minutos, uma tabela `pedido_status`, um consumidor de fila e um e-mail do atendimento quando trava. Deixe o resto do sistema como está.

## 2. Escreva a function como uma sequência linear de etapas

Escreva a sequência de etapas como um método, do jeito que você descreveria para um colega. Cada etapa que grava, chama ou envia algo é uma chamada; cada espera por algo externo é uma espera por evento; cada prazo, um delay.

{% tabs %}
{% tab title="C#" %}

```csharp
using Skail.Platform.Runtime;
using Skail.Platform.Runtime.Threading;

public static class Eventos
{
    public const string PagamentoConfirmado = "PAGAMENTO_CONFIRMADO";
    public const string PedidoEntregue      = "PEDIDO_ENTREGUE";
}

public sealed class FulfillmentService
{
    private readonly ErpDbContext _db;
    public FulfillmentService(ErpDbContext db) => _db = db;

    [SkailFunction]
    public async SkailTask ProcessarPedido(Guid pedidoId)
    {
        await ReservarEstoqueNoErp(pedidoId);

        var pagamento = SkailTask.WaitForEvent<PagamentoConfirmado>(Eventos.PagamentoConfirmado, pedidoId.ToString());
        var prazoPagamento = SkailTask.Delay(TimeSpan.FromDays(2));
        if (await SkailTask.WhenAny(pagamento, prazoPagamento) == prazoPagamento)
        {
            await LiberarEstoqueNoErp(pedidoId);
            return;
        }

        var etiqueta = await GerarEtiquetaDeEnvio(pedidoId, (await pagamento).FormaPagamento);

        var entrega = await SkailTask.WaitForEvent<ConfirmacaoEntrega>(Eventos.PedidoEntregue, etiqueta.CodigoRastreio);
        await AtualizarStatusNoErp(pedidoId, entrega);
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
// fluxo.ts
import { Skail, SkailFunction } from "@skail/runtime";
import { FulfillmentCommands } from "./comandos.js";
import type { ConfirmacaoEntrega, PagamentoConfirmado } from "./tipos.js";

export const Eventos = {
  PagamentoConfirmado: "PAGAMENTO_CONFIRMADO",
  PedidoEntregue: "PEDIDO_ENTREGUE",
} as const;

export class FulfillmentService {
  private readonly cmd = new FulfillmentCommands();

  @SkailFunction()
  async ProcessarPedido(pedidoId: string): Promise<void> {
    await this.cmd.ReservarEstoqueNoErp(pedidoId);

    const pagamento = Skail.waitForEvent<PagamentoConfirmado>(Eventos.PagamentoConfirmado, pedidoId);
    const prazoPagamento = Skail.delay(2 * 24 * 60 * 60 * 1000);
    if ((await Skail.whenAny(pagamento, prazoPagamento)) === undefined) {
      await this.cmd.LiberarEstoqueNoErp(pedidoId);
      return;
    }

    const etiqueta = await this.cmd.GerarEtiquetaDeEnvio(pedidoId, (await pagamento).FormaPagamento);

    const entrega = await Skail.waitForEvent<ConfirmacaoEntrega>(Eventos.PedidoEntregue, etiqueta.CodigoRastreio);
    await this.cmd.AtualizarStatusNoErp(pedidoId, entrega);
  }
}
```

{% endtab %}
{% endtabs %}

O job de cinco minutos, a tabela de status como máquina de estados e o consumidor de fila desaparecem: a function é a máquina de estados, e o histórico da execução é a tabela.

## 3. Extraia as operações de I/O para commands

Cada trecho que toca banco, HTTP ou gera valores ganha um command próprio: um método público e `async`, marcado como command. O código dentro dele é o que você já tinha; a diferença é que esse trecho passa a ficar em um command.

{% tabs %}
{% tab title="C#" %}

```csharp
    [SkailCommand]
    public async SkailTask ReservarEstoqueNoErp(Guid pedidoId)
    {
        var itens = await _db.PedidoItens.Where(i => i.PedidoId == pedidoId).ToListAsync();
        foreach (var item in itens)
        {
            var estoque = await _db.Estoques.SingleAsync(e => e.ProdutoId == item.ProdutoId);
            estoque.QuantidadeDisponivel -= item.Quantidade;
            estoque.QuantidadeReservada  += item.Quantidade;
        }
        var pedido = await _db.Pedidos.SingleAsync(p => p.Id == pedidoId);
        pedido.Status = "estoque_reservado";
        await _db.SaveChangesAsync();
    }

    [SkailCommand]
    public async SkailTask<EtiquetaGerada> GerarEtiquetaDeEnvio(Guid pedidoId, string formaPagamento)
    {
        var codigoRastreio = $"BR-{pedidoId:N}";   // único por pedido: vira o instanceId de PEDIDO_ENTREGUE
        _db.EtiquetasEnvio.Add(new EtiquetaEnvioEntity
        {
            Id = Guid.NewGuid(), PedidoId = pedidoId, CodigoRastreio = codigoRastreio,
            FormaPagamento = formaPagamento, CriadaEm = DateTime.UtcNow   // relógio e Guid: permitidos aqui, dentro do command
        });
        var pedido = await _db.Pedidos.SingleAsync(p => p.Id == pedidoId);
        pedido.Status = "etiqueta_gerada";
        await _db.SaveChangesAsync();
        return new EtiquetaGerada(codigoRastreio);
    }

    [SkailCommand]
    public async SkailTask AtualizarStatusNoErp(Guid pedidoId, ConfirmacaoEntrega entrega)
    {
        var pedido = await _db.Pedidos.SingleAsync(p => p.Id == pedidoId);
        pedido.Status = "entregue";
        pedido.EntregueEm = entrega.DataHora;
        await _db.SaveChangesAsync();
    }

    [SkailCommand]
    public async SkailTask LiberarEstoqueNoErp(Guid pedidoId) { /* inverso de ReservarEstoqueNoErp */ }
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
// tipos.ts
export interface PagamentoConfirmado {
  FormaPagamento: string;
}
export interface ConfirmacaoEntrega {
  DataHora: string;
}
export interface EtiquetaGerada {
  CodigoRastreio: string;
}
```

```typescript
// comandos.ts
import { SkailCommand } from "@skail/runtime";
import { ErpDb, type EstoqueEntity, type PedidoEntity, type PedidoItemEntity } from "./erp-db.js";
import type { ConfirmacaoEntrega, EtiquetaGerada } from "./tipos.js";

export class FulfillmentCommands {
  private readonly db = new ErpDb();

  @SkailCommand()
  async ReservarEstoqueNoErp(pedidoId: string): Promise<void> {
    await this.db.transaction(async (tx) => {
      const itens = await tx.query<PedidoItemEntity>("SELECT * FROM pedido_itens WHERE pedido_id = $1", [pedidoId]);
      for (const item of itens) {
        const [estoque] = await tx.query<EstoqueEntity>("SELECT * FROM estoques WHERE produto_id = $1", [item.produto_id]);
        await tx.query(
          "UPDATE estoques SET quantidade_disponivel = $1, quantidade_reservada = $2 WHERE produto_id = $3",
          [estoque!.quantidade_disponivel - item.quantidade, estoque!.quantidade_reservada + item.quantidade, item.produto_id],
        );
      }
      await tx.query("UPDATE pedidos SET status = 'estoque_reservado' WHERE id = $1", [pedidoId]);
    });
  }

  @SkailCommand()
  async GerarEtiquetaDeEnvio(pedidoId: string, formaPagamento: string): Promise<EtiquetaGerada> {
    const agora = new Date(); // relógio: permitido aqui, dentro do command
    const codigoRastreio = `BR-${pedidoId.replace(/-/g, "")}`; // único por pedido: vira o instanceId de PEDIDO_ENTREGUE
    await this.db.transaction(async (tx) => {
      await tx.query(
        `INSERT INTO etiquetas_envio (id, pedido_id, codigo_rastreio, forma_pagamento, criada_em)
         VALUES ($1, $2, $3, $4, $5)`,
        [crypto.randomUUID(), pedidoId, codigoRastreio, formaPagamento, agora.toISOString()],
      );
      await tx.query("UPDATE pedidos SET status = 'etiqueta_gerada' WHERE id = $1", [pedidoId]);
    });
    return { CodigoRastreio: codigoRastreio };
  }

  @SkailCommand()
  async AtualizarStatusNoErp(pedidoId: string, entrega: ConfirmacaoEntrega): Promise<void> {
    const [pedido] = await this.db.query<PedidoEntity>("SELECT * FROM pedidos WHERE id = $1", [pedidoId]);
    if (!pedido) throw new Error(`Pedido ${pedidoId} não encontrado`);
    await this.db.query("UPDATE pedidos SET status = 'entregue', entregue_em = $1 WHERE id = $2", [entrega.DataHora, pedidoId]);
  }

  @SkailCommand()
  async LiberarEstoqueNoErp(pedidoId: string): Promise<void> {
    /* inverso de ReservarEstoqueNoErp */
  }
}
```

{% endtab %}
{% endtabs %}

Ao extrair, aplique três critérios:

1. se o método faz I/O ou lê o relógio, ele passa a ser command;
2. se o método só decide sobre valores já conhecidos, ele fica na function;
3. se o método recebe ou devolve uma entidade de ORM, troque por id e tipo simples ([Como passar dados](/construa/como-fazer/passar-dados-entre-funcao-e-commands.md)).

E lembre que `ReservarEstoqueNoErp` e `GerarEtiquetaDeEnvio` precisam ser idempotentes: se rodarem duas vezes por uma falha na gravação, o estoque não pode ser reservado em dobro, e a etiqueta já gerada para o pedido precisa ser devolvida em vez de gravada de novo (o exemplo de [Modernizar um sistema legado](/construa/casos-de-uso/modernizar-um-sistema-legado.md) faz essa consulta) ([Como escrever commands idempotentes](/construa/como-fazer/escrever-commands-idempotentes.md)).

## 4. Conecte o disparo e os eventos

Quem iniciava o fluxo (a tela que fecha o pedido) passa a fazer `POST /api/v1/trigger/erp/v1.0.0/ProcessarPedido/{execucaoId}` com `["{pedidoId}"]`. O `execucaoId` precisa ser um GUID: a tela gera o GUID uma vez, reutiliza o mesmo valor nas novas tentativas desse disparo e guarda a relação entre ele e o pedido (por exemplo, numa coluna de `pedidos`), para achar a execução no Monitor depois. Quem sinalizava o pagamento (o webhook do gateway) passa a fazer `POST /api/v1/fire/PAGAMENTO_CONFIRMADO/{pedidoId}` com o payload. A transportadora faz o mesmo com `PEDIDO_ENTREGUE` e o código de rastreio. Ver [Como disparar uma função](/construa/como-fazer/disparar-uma-funcao-pela-api.md) e [Como disparar um evento](/construa/como-fazer/disparar-um-evento-pela-api.md).

O que sai do sistema:

* o job agendado;
* a tabela de controle do fluxo (a tabela de negócio `pedidos` fica);
* o consumidor de fila desse fluxo;
* o código de retry das chamadas que viraram commands (as novas tentativas passam a ser do runtime).

O que fica: tudo o mais. Se o fluxo decide pelo tipo da exceção (entrar em contingência, recusa sinalizada por exceção), esse tratamento também fica, agora na function, com `retryCount: 0` no command. Ver [Como tratar erros e compensar](/construa/como-fazer/tratar-erros-e-desfazer-passos.md).

## O que muda nas assinaturas e nas esperas

{% tabs %}
{% tab title="C#" %}
Nas assinaturas dos métodos decorados, `Task` passa a ser `SkailTask` e `Task<T>` passa a ser `SkailTask<T>`; o resto do código é igual. Dentro dos commands, `Task.Delay`, `Task.WhenAll` e `HttpClient` continuam iguais. Dentro da function, `Task.Delay` passa a ser `SkailTask.Delay`, `Task.WhenAll` passa a ser `SkailTask.WhenAll`, e qualquer I/O ou relógio vai para um command. Métodos auxiliares `async Task` que a function chamava e que fazem I/O também passam a ser commands; se são lógica pura, ficam como métodos síncronos comuns.
{% endtab %}

{% tab title="TypeScript" %}
As assinaturas continuam `async`, retornando `Promise<T>`; o que muda é a marcação com `@SkailFunction()` e `@SkailCommand()`, em métodos públicos e de instância. Dentro dos commands, o código continua igual; só os módulos Node de I/O entram com `await import(...)` dentro do command, porque o build rejeita import estático deles. Dentro da function, uma pausa passa a ser `Skail.delay`, e a espera por várias chamadas passa a ser `Skail.whenAll` ou `Skail.whenAny` (só promises do runtime entram nelas; `Promise.all`, `Promise.race` e `.then`, não). Qualquer I/O, relógio ou valor aleatório vai para um command: dentro da function, `fetch()`, `new Date()`, `Date.now()`, `Math.random()`, `crypto.randomUUID()` e `setTimeout()` lançam erro na hora. Métodos auxiliares `async` que a function chamava e que fazem I/O também passam a ser commands; se são lógica pura, ficam como métodos síncronos comuns.
{% endtab %}
{% endtabs %}

## Avance aos poucos

Migre um fluxo por vez, começando pelo que mais dá problema. Cada fluxo migrado é independente dos outros; o sistema continua inteiro enquanto isso. O exemplo completo, com o cenário de negócio e a operação no Monitor, está em [Modernização de um legado](/construa/casos-de-uso/modernizar-um-sistema-legado.md).

## Próximos passos

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-thumbs-up">:thumbs-up:</i></td><td><strong>Padrões recomendados</strong></td><td>Antes do primeiro pull request.</td><td><a href="/construa/como-fazer/padroes-e-antipadroes/padroes-recomendados.md">Padrões recomendados</a></td></tr><tr><td><i class="fa-vial">:vial:</i></td><td><strong>Como testar functions</strong></td><td>Para cobrir o fluxo migrado.</td><td><a href="/construa/como-fazer/testar-seus-fluxos.md">Testar seus fluxos</a></td></tr><tr><td><i class="fa-cloud">:cloud:</i></td><td><strong>Como hospedar</strong></td><td>Para decidir se o runtime roda no processo do sistema ou em um worker ao lado.</td><td><a href="/opere/configurar-e-hospedar/container-kubernetes-e-azure.md">Container, Kubernetes e Azure</a></td></tr></tbody></table>
