> 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/cobranca-recorrencia-e-split-de-pagamento.md).

# Cobrança, recorrência e split de pagamento

Exemplos de códigos de billing: cobrança, retries, recorrência, período de graça, estorno e split de pagamento

Faturamento é um dos lugares onde sistemas costumam ficar mais complexos do que deveriam.

Em geral, há muito mais código do que a regra de negócio exige.

Você precisa:

* tentar cobrar
* esperar resposta do gateway
* tentar de novo quando falha
* acompanhar o cliente ao longo de dias
* aplicar multa
* suspender serviço
* reativar quando o pagamento chega

> **No legado, isso normalmente vira jobs, tabelas de controle e muita lógica espalhada.**
>
> <mark style="background-color:$success;">No skail, isso tudo é um método simples.</mark>

### Cobrança com retentativas (backoff)

No primeiro cenário, você tenta cobrar o cliente até 4 vezes antes de desistir.

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

```csharp
[SkailFunction]
public async SkailTask<ResultadoCobranca> CobrarCartao(Guid faturaId)
{
    var backoff = new[]
    {
        TimeSpan.Zero,
        TimeSpan.FromMinutes(5),
        TimeSpan.FromHours(1),
        TimeSpan.FromHours(6)
    };

    foreach (var espera in backoff)
    {
        await SkailTask.Delay(espera);

        var resultado = await TentarCobranca(faturaId);

        if (resultado.Sucesso)
        {
            await ConfirmarPagamento(faturaId, resultado.TransacaoId);
            return ResultadoCobranca.Sucesso;
        }

        if (resultado.ErroPermanente)
        {
            await MarcarComoRecusada(faturaId, resultado.Motivo);
            return ResultadoCobranca.Recusada;
        }
    }

    await EscalarParaCobrancaManual(faturaId);
    return ResultadoCobranca.Pendente;
}
```

{% endcode %}
{% endtab %}

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

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";
import { faturamento, type ResultadoCobranca, type TentativaCobranca } from "./faturamento-integracoes.js";

export class Cobranca {
  @SkailFunction()
  async CobrarCartao(faturaId: string): Promise<ResultadoCobranca> {
    const backoff = [
      0,
      5 * 60_000,
      60 * 60_000,
      6 * 60 * 60_000,
    ];

    for (const espera of backoff) {
      await Skail.delay(espera);

      const resultado = await this.TentarCobranca(faturaId);

      if (resultado.Sucesso) {
        await this.ConfirmarPagamento(faturaId, resultado.TransacaoId);
        return "Sucesso";
      }

      if (resultado.ErroPermanente) {
        await this.MarcarComoRecusada(faturaId, resultado.Motivo);
        return "Recusada";
      }
    }

    await this.EscalarParaCobrancaManual(faturaId);
    return "Pendente";
  }

  @SkailCommand()
  async TentarCobranca(faturaId: string): Promise<TentativaCobranca> {
    return faturamento.TentarCobranca(faturaId);
  }

  @SkailCommand()
  async ConfirmarPagamento(faturaId: string, transacaoId: string): Promise<void> {
    await faturamento.ConfirmarPagamento(faturaId, transacaoId);
  }

  @SkailCommand()
  async MarcarComoRecusada(faturaId: string, motivo: string): Promise<void> {
    await faturamento.MarcarComoRecusada(faturaId, motivo);
  }

  @SkailCommand()
  async EscalarParaCobrancaManual(faturaId: string): Promise<void> {
    await faturamento.EscalarParaCobrancaManual(faturaId);
  }
}
```

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

Neste exemplo fazemos 4 tentativas com espaçamento crescente.

Entre uma tentativa e outra, a execução é hibernada: não existe thread nem processo ocupado, e você não consome nenhum recurso enquanto aguarda.

Quando o tempo passa, o skail retoma automaticamente o código do ponto onde parou (como se ele nunca tivesse parado).

Os métodos que chamam o gateway ou o banco (`TentarCobranca`, `ConfirmarPagamento`, `MarcarComoRecusada`, `EscalarParaCobrancaManual`) são commands. Um command que já gravou resultado não roda de novo quando a função é retomada, por mais vezes que isso aconteça ao longo das horas.<br>

### Régua de cobrança

Agora vamos além da cobrança: você precisa acompanhar a fatura ao longo do tempo e cuidar de seu ciclo de vida:

* enviar lembrete antes do vencimento
* avisar no vencimento
* aplicar multa
* suspender o serviço
* reativar quando o pagamento chegar

E tudo isso deve parar imediatamente se o pagamento chegar.

<figure><img src="https://1898946878-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsJmS5ILPG5x2x5j0WyiV%2Fuploads%2FKE4M79O8VJpcvwJ7xYJK%2Fap-faturamento-regua.svg?alt=media" alt="AcompanharFatura de cima para baixo: lê a data atual, espera até 3 dias antes do vencimento, envia o lembrete, lê a data de novo, espera o vencimento, avisa, espera 5 dias, aplica a multa, espera 10 dias e suspende o serviço. Uma espera só pelo evento PAGAMENTO_CONFIRMADO concorre com cada prazo e, depois da suspensão, continua sem prazo. Se o pagamento chega antes da suspensão, RegistrarPagamento grava e a function retorna; se chega depois, grava e ReativarServico reativa o serviço."><figcaption></figcaption></figure>

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

```csharp
[SkailFunction]
public async SkailTask AcompanharFatura(Guid faturaId, DateTime vencimento)
{
    var pagamento = SkailTask.WaitForEvent<PagamentoConfirmado>("PAGAMENTO_CONFIRMADO", faturaId.ToString());

    var agora = await ObterDataAtual();
    await SkailTask.WhenAny(pagamento, SkailTask.Delay(vencimento.AddDays(-3) - agora));
    if (pagamento.IsCompleted) { await RegistrarPagamento(faturaId, await pagamento); return; }

    await EnviarLembrete(faturaId);

    agora = await ObterDataAtual();
    await SkailTask.WhenAny(pagamento, SkailTask.Delay(vencimento - agora));
    if (pagamento.IsCompleted) { await RegistrarPagamento(faturaId, await pagamento); return; }

    await EnviarAvisoVencimento(faturaId);

    await SkailTask.WhenAny(pagamento, SkailTask.Delay(TimeSpan.FromDays(5)));
    if (pagamento.IsCompleted) { await RegistrarPagamento(faturaId, await pagamento); return; }

    await AplicarMulta(faturaId);

    await SkailTask.WhenAny(pagamento, SkailTask.Delay(TimeSpan.FromDays(10)));
    if (pagamento.IsCompleted) { await RegistrarPagamento(faturaId, await pagamento); return; }

    await SuspenderServico(faturaId);

    // Serviço suspenso, fatura ainda em aberto: a execução segue esperando o pagamento, sem prazo.
    await RegistrarPagamento(faturaId, await pagamento);
    await ReativarServico(faturaId);
}

[SkailCommand]
public async SkailTask<DateTime> ObterDataAtual()
{
    // Leitura de relógio é não determinística: fica em um command para o resultado ser gravado e reaproveitado no replay.
    return DateTime.UtcNow;
}

[SkailCommand]
public async SkailTask RegistrarPagamento(Guid faturaId, PagamentoConfirmado pagamento)
{
    await _pagamentos.Salvar(faturaId, pagamento);
}

public record PagamentoConfirmado(string TransacaoId, decimal Valor, DateTime PagoEm);
```

{% endcode %}
{% endtab %}

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

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";
import { faturamento, type PagamentoConfirmado } from "./faturamento-integracoes.js";

const DiaMs = 24 * 60 * 60_000;

function ate(alvo: string, agora: string, ajusteMs = 0): number {
  return Math.max(0, new Date(alvo).getTime() + ajusteMs - new Date(agora).getTime());
}

export class Regua {
  @SkailFunction()
  async AcompanharFatura(faturaId: string, vencimento: string): Promise<void> {
    const pagamento = Skail.waitForEvent<PagamentoConfirmado>("PAGAMENTO_CONFIRMADO", faturaId);

    let agora = await this.ObterDataAtual();
    let pago = await Skail.whenAny(pagamento, Skail.delay(ate(vencimento, agora, -3 * DiaMs)));
    if (pago !== undefined) { await this.RegistrarPagamento(faturaId, pago); return; }

    await this.EnviarLembrete(faturaId);

    agora = await this.ObterDataAtual();
    pago = await Skail.whenAny(pagamento, Skail.delay(ate(vencimento, agora)));
    if (pago !== undefined) { await this.RegistrarPagamento(faturaId, pago); return; }

    await this.EnviarAvisoVencimento(faturaId);

    pago = await Skail.whenAny(pagamento, Skail.delay(5 * DiaMs));
    if (pago !== undefined) { await this.RegistrarPagamento(faturaId, pago); return; }

    await this.AplicarMulta(faturaId);

    pago = await Skail.whenAny(pagamento, Skail.delay(10 * DiaMs));
    if (pago !== undefined) { await this.RegistrarPagamento(faturaId, pago); return; }

    await this.SuspenderServico(faturaId);

    // Serviço suspenso, fatura ainda em aberto: a execução segue esperando o pagamento, sem prazo.
    await this.RegistrarPagamento(faturaId, await pagamento);
    await this.ReativarServico(faturaId);
  }

  @SkailCommand()
  async ObterDataAtual(): Promise<string> {
    // Leitura de relógio é não determinística: fica em um command para o resultado ser gravado e reaproveitado no replay.
    return new Date().toISOString();
  }

  @SkailCommand()
  async RegistrarPagamento(faturaId: string, pagamento: PagamentoConfirmado): Promise<void> {
    await faturamento.SalvarPagamento(faturaId, pagamento);
  }

  @SkailCommand()
  async EnviarLembrete(faturaId: string): Promise<void> {
    await faturamento.EnviarLembrete(faturaId);
  }

  @SkailCommand()
  async EnviarAvisoVencimento(faturaId: string): Promise<void> {
    await faturamento.EnviarAvisoVencimento(faturaId);
  }

  @SkailCommand()
  async AplicarMulta(faturaId: string): Promise<void> {
    await faturamento.AplicarMulta(faturaId);
  }

  @SkailCommand()
  async SuspenderServico(faturaId: string): Promise<void> {
    await faturamento.SuspenderServico(faturaId);
  }

  @SkailCommand()
  async ReativarServico(faturaId: string): Promise<void> {
    await faturamento.ReativarServico(faturaId);
  }

}
```

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

O fluxo fica explícito:

* você espera duas coisas ao mesmo tempo: o pagamento ou algum tempo
* o que acontecer primeiro define o próximo passo
* quando o pagamento chega, a própria execução grava o pagamento (`RegistrarPagamento`) e encerra a régua

{% hint style="info" %}
Repare que a leitura do relógio (`ObterDataAtual`) está em um command, e não direto na função. O corpo de uma function é reexecutado a cada retomada e precisa produzir sempre o mesmo resultado; ler a hora atual ali quebraria essa regra. Dentro do command, o valor é lido uma vez e gravado.
{% endhint %}

#### Como isso funciona na prática

A [espera por evento](/referencia/sdk-csharp/waitforevent.md#comportamento) faz o código “esperar” um evento externo (ex: pagamento confirmado) e entrega o payload do evento quando ele chega.

Lembrando: durante essa espera nada fica rodando e nenhum recurso (CPU, processo, thread) é consumido.

Quando o evento chega, a execução continua exatamente de onde parou.

**Conclusão:** Se o pagamento chega antes do lembrete, no dia do vencimento ou depois da multa, o método grava o pagamento e retorna; nenhuma das etapas seguintes executa. Se chega depois da suspensão, a execução ainda está lá esperando: grava o pagamento e reativa o serviço.

Se não chega, esperamos alguns dias e executamos a regra de negócio seguinte: enviar lembrete, avisar, aplicar multa, etc.

<mark style="background-color:$success;">O fluxo dispensa crontab, agendador de jobs e tabela de próxima ação.</mark>

O método descreve todas as etapas da fatura de forma clara.<br>

### Conectando com o gateway de pagamento

Quando o gateway avisa que o pagamento foi confirmado (por webhook, por callback assíncrono, por notificação do adquirente), o endpoint que recebe esse aviso repassa o payload ao skail, disparando o evento na [API HTTP de eventos do skail](/referencia/api-http/post-fire.md). Ele não grava nada:

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

```csharp
// No endpoint que recebe o webhook do gateway (código ASP.NET comum, fora do skail)
// _http já tem os headers skail-key e Skail-namespace configurados
var resposta = await _http.PostAsJsonAsync(
    $"{skailBaseUrl}/api/v1/fire/PAGAMENTO_CONFIRMADO/{faturaId}",
    new PagamentoConfirmado(dados.TransacaoId, dados.Valor, dados.PagoEm));

// Resposta fora de 2xx lança exceção: o endpoint devolve erro e o gateway reenvia o webhook
resposta.EnsureSuccessStatusCode();
```

{% endcode %}
{% endtab %}

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

```typescript
import type { PagamentoConfirmado } from "./faturamento-integracoes.js";

const skailBaseUrl = process.env.SKAIL_BASE_URL!;

// Headers exigidos pela API do skail.
const headers = {
  "Content-Type": "application/json",
  "skail-key": process.env.SKAIL_KEY!,
  "Skail-namespace": process.env.SKAIL_NAMESPACE!,
};

// No endpoint que recebe o webhook do gateway (código Node comum, fora do skail)
export async function aoReceberWebhookDoGateway(faturaId: string, dados: PagamentoConfirmado): Promise<void> {
  const resposta = await fetch(`${skailBaseUrl}/api/v1/fire/PAGAMENTO_CONFIRMADO/${faturaId}`, {
    method: "POST",
    headers,
    body: JSON.stringify({ TransacaoId: dados.TransacaoId, Valor: dados.Valor, PagoEm: dados.PagoEm }),
  });

  // Resposta fora de 2xx lança exceção: o endpoint devolve erro e o gateway reenvia o webhook
  if (!resposta.ok) {
    throw new Error(`Falha ao disparar PAGAMENTO_CONFIRMADO: HTTP ${resposta.status}`);
  }
}
```

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

Toda execução aguardando aquela fatura retoma com o payload em mãos, e é lá dentro, no command `RegistrarPagamento`, que o pagamento vai para o banco. Isso é de propósito. Se o endpoint gravasse no banco e só depois disparasse o evento, uma falha entre os dois passos deixaria a fatura paga no banco e a régua ainda em andamento, com lembrete e multa a caminho, sem ninguém perceber. 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 e grava assim que voltar. O webhook fica com uma única responsabilidade, entregar o aviso. Se o POST falhar, devolva erro ao gateway para que ele reenvie o webhook.

Quem dispara o evento não sabe quem está aguardando, pouco importa.<br>

* Pode ser a `AcompanharFatura` no meio da régua de cobrança.
* Pode ser uma função de liberação de acesso esperando o OK para liberar um curso comprado.
* Pode ser esperando alguém aprovar um pedido com valor acima de R$10.000, etc.

\
Não existe acoplamento direto entre quem envia e quem recebe: a conexão é feita pelo evento. E o disparo é só um POST: pode vir de qualquer sistema, em qualquer linguagem.

### Estorno com prazo de análise

Quando o cliente solicita um estorno, a política da empresa aguarda 48 horas antes de processar: um período em que o próprio cliente pode desistir da solicitação ou o time de atendimento pode intervir em casos suspeitos. Se nada acontecer nesse prazo, o estorno segue automaticamente para o gateway de pagamento.

Um cenário clássico com decisão humana + tempo:

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

```csharp
[SkailFunction]
public async SkailTask ProcessarEstorno(Guid estornoId, Guid pagamentoId)
{
    await RegistrarSolicitacao(estornoId, pagamentoId);

    var cancelamento = SkailTask.WaitForEvent<MotivoCancelamento>(
        "ESTORNO_CANCELADO",
        estornoId.ToString()
    );
    var prazoDeAnalise = SkailTask.Delay(TimeSpan.FromHours(48));

    await SkailTask.WhenAny(cancelamento, prazoDeAnalise);

    if (cancelamento.IsCompleted)
    {
        var motivo = await cancelamento;
        await ArquivarEstorno(estornoId, motivo);
        return;
    }

    var transacao = await SolicitarEstornoNoGateway(pagamentoId);

    var confirmacao = SkailTask.WaitForEvent<ConfirmacaoEstorno>(
        "ESTORNO_PROCESSADO",
        transacao.Id
    );
    var prazoDoGateway = SkailTask.Delay(TimeSpan.FromDays(7));

    await SkailTask.WhenAny(confirmacao, prazoDoGateway);

    if (!confirmacao.IsCompleted)
    {
        await EscalarEstornoNaoConfirmado(estornoId, transacao.Id);
        return;
    }

    await FinalizarEstorno(estornoId, await confirmacao);
}
```

{% endcode %}
{% endtab %}

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

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";
import {
  faturamento,
  type ConfirmacaoEstorno,
  type MotivoCancelamento,
  type TransacaoEstorno,
} from "./faturamento-integracoes.js";

const HoraMs = 60 * 60_000;
const DiaMs = 24 * HoraMs;

export class Estornos {
  @SkailFunction()
  async ProcessarEstorno(estornoId: string, pagamentoId: string): Promise<void> {
    await this.RegistrarSolicitacao(estornoId, pagamentoId);

    const cancelamento = Skail.waitForEvent<MotivoCancelamento>("ESTORNO_CANCELADO", estornoId);
    const prazoDeAnalise = Skail.delay(48 * HoraMs);

    const motivo = await Skail.whenAny(cancelamento, prazoDeAnalise);

    if (motivo !== undefined) {
      await this.ArquivarEstorno(estornoId, motivo);
      return;
    }

    const transacao = await this.SolicitarEstornoNoGateway(pagamentoId);

    const confirmacao = Skail.waitForEvent<ConfirmacaoEstorno>("ESTORNO_PROCESSADO", transacao.Id);
    const prazoDoGateway = Skail.delay(7 * DiaMs);

    const confirmado = await Skail.whenAny(confirmacao, prazoDoGateway);

    if (confirmado === undefined) {
      await this.EscalarEstornoNaoConfirmado(estornoId, transacao.Id);
      return;
    }

    await this.FinalizarEstorno(estornoId, confirmado);
  }

  @SkailCommand()
  async RegistrarSolicitacao(estornoId: string, pagamentoId: string): Promise<void> {
    await faturamento.RegistrarSolicitacao(estornoId, pagamentoId);
  }

  @SkailCommand()
  async ArquivarEstorno(estornoId: string, motivo: MotivoCancelamento): Promise<void> {
    await faturamento.ArquivarEstorno(estornoId, motivo);
  }

  @SkailCommand()
  async SolicitarEstornoNoGateway(pagamentoId: string): Promise<TransacaoEstorno> {
    return faturamento.SolicitarEstornoNoGateway(pagamentoId);
  }

  @SkailCommand()
  async EscalarEstornoNaoConfirmado(estornoId: string, transacaoId: string): Promise<void> {
    await faturamento.EscalarEstornoNaoConfirmado(estornoId, transacaoId);
  }

  @SkailCommand()
  async FinalizarEstorno(estornoId: string, confirmacao: ConfirmacaoEstorno): Promise<void> {
    await faturamento.FinalizarEstorno(estornoId, confirmacao);
  }

}
```

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

<mark style="background-color:$success;">Mesmo com horas ou dias de espera, o código continua simples.</mark>

Esse código tem dois momentos de espera:\
\
Primeiro, você aguarda até 48 horas (com um [delay](/referencia/sdk-csharp/delay.md)) para o cliente ou o time decidirem. Depois, usa uma [espera por evento](/referencia/sdk-csharp/waitforevent.md#comportamento) para aguardar a confirmação do gateway, que pode chegar em minutos, horas ou até dias. Essa espera também tem prazo: se a confirmação não chegar em 7 dias, o estorno é escalado para análise (`EscalarEstornoNaoConfirmado`).

Em ambos os casos, sua aplicação simplesmente hiberna (pausa) e continua do mesmo ponto quando algo acontece.<br>

### Split de pagamento em marketplace

Quando o pagamento de um pedido (multi-seller neste exemplo) é confirmado, você precisa orquestrar o split entre as partes (plataforma, logística, cada vendedor) e reter a parcela de cada vendedor até a confirmação de entrega, para cobrir a janela de cancelamento/disputa.

Cada item tem os próprios prazos e etapas, mas o pedido só é considerado concluído quando tudo termina.

<figure><img src="https://1898946878-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsJmS5ILPG5x2x5j0WyiV%2Fuploads%2FRAMoF9zyOCu0S6fzITlN%2Fap-faturamento-split.svg?alt=media" alt="ProcessarPedido carrega o pedido, libera a taxa da plataforma e o frete e dispara uma function filha ProcessarRepasseItem para cada item. Cada uma retém a parcela do vendedor, espera a entrega com prazo de 7 dias e a janela de disputa de 7 dias e libera a parcela do vendedor; se o prazo de entrega vence, roda EscalarEntregaNaoConfirmada, e se houver disputa, roda TratarDisputa. O pedido espera todos os itens e termina com MarcarPedidoComoFinalizado."><figcaption></figcaption></figure>

Veja como seu código agora fica claro e óbvio de manter (sem medo!):

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

```csharp
[SkailFunction]
public async SkailTask ProcessarPedido(Guid pedidoId)
{
    var pedido = await CarregarPedido(pedidoId);

    await LiberarParaPlataforma(pedido.TaxaPlataforma);
    await LiberarParaLogistica(pedido.Frete);

    var liquidacoes = pedido.Itens
        .Select(item => ProcessarRepasseItem(pedidoId, item.SellerId, item.ItemId, item.Valor))
        .ToArray();

    await SkailTask.WhenAll(liquidacoes);

    await MarcarPedidoComoFinalizado(pedidoId);
}

[SkailFunction]
public async SkailTask ProcessarRepasseItem(Guid pedidoId, Guid sellerId, Guid itemId, decimal valor)
{
    await ReservarValorParaSeller(sellerId, itemId, valor);

    var entrega = SkailTask.WaitForEvent("ITEM_ENTREGUE", itemId.ToString());
    var prazoDeEntrega = SkailTask.Delay(TimeSpan.FromDays(7));

    await SkailTask.WhenAny(entrega, prazoDeEntrega);

    if (!entrega.IsCompleted)
    {
        await EscalarEntregaNaoConfirmada(pedidoId, itemId);
        return;
    }

    var disputa = SkailTask.WaitForEvent<MotivoDisputa>("DISPUTA_ABERTA", itemId.ToString());
    var janelaDeContestacao = SkailTask.Delay(TimeSpan.FromDays(7));

    await SkailTask.WhenAny(disputa, janelaDeContestacao);

    if (disputa.IsCompleted)
    {
        var motivo = await disputa;
        await TratarDisputa(pedidoId, itemId, motivo);
        return;
    }

    await LiberarPagamentoParaSeller(sellerId, itemId, valor);
}
```

{% endcode %}
{% endtab %}

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

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";
import { faturamento, type PedidoMarketplace } from "./faturamento-integracoes.js";

const DiaMs = 24 * 60 * 60_000;

export interface ItemEntregue {
  EntregueEm?: string;
}

export interface MotivoDisputa {
  Descricao: string;
}

export class Marketplace {
  @SkailFunction()
  async ProcessarPedido(pedidoId: string): Promise<void> {
    const pedido = await this.CarregarPedido(pedidoId);

    await this.LiberarParaPlataforma(pedido.TaxaPlataforma);
    await this.LiberarParaLogistica(pedido.Frete);

    const liquidacoes = pedido.Itens.map((item) =>
      this.ProcessarRepasseItem(pedidoId, item.SellerId, item.ItemId, item.Valor),
    );

    await Skail.whenAll(liquidacoes);

    await this.MarcarPedidoComoFinalizado(pedidoId);
  }

  @SkailFunction()
  async ProcessarRepasseItem(pedidoId: string, sellerId: string, itemId: string, valor: number): Promise<void> {
    await this.ReservarValorParaSeller(sellerId, itemId, valor);

    const entrega = Skail.waitForEvent<ItemEntregue>("ITEM_ENTREGUE", itemId);
    const prazoDeEntrega = Skail.delay(7 * DiaMs);

    const entregue = await Skail.whenAny(entrega, prazoDeEntrega);

    if (entregue === undefined) {
      await this.EscalarEntregaNaoConfirmada(pedidoId, itemId);
      return;
    }

    const disputa = Skail.waitForEvent<MotivoDisputa>("DISPUTA_ABERTA", itemId);
    const janelaDeContestacao = Skail.delay(7 * DiaMs);

    const motivo = await Skail.whenAny(disputa, janelaDeContestacao);

    if (motivo !== undefined) {
      await this.TratarDisputa(pedidoId, itemId, motivo.Descricao);
      return;
    }

    await this.LiberarPagamentoParaSeller(sellerId, itemId, valor);
  }

  @SkailCommand()
  async CarregarPedido(pedidoId: string): Promise<PedidoMarketplace> {
    return faturamento.CarregarPedido(pedidoId);
  }

  @SkailCommand()
  async LiberarParaPlataforma(valor: number): Promise<void> {
    await faturamento.LiberarParaPlataforma(valor);
  }

  @SkailCommand()
  async LiberarParaLogistica(valor: number): Promise<void> {
    await faturamento.LiberarParaLogistica(valor);
  }

  @SkailCommand()
  async MarcarPedidoComoFinalizado(pedidoId: string): Promise<void> {
    await faturamento.MarcarPedidoComoFinalizado(pedidoId);
  }

  @SkailCommand()
  async ReservarValorParaSeller(sellerId: string, itemId: string, valor: number): Promise<void> {
    await faturamento.ReservarValorParaSeller(sellerId, itemId, valor);
  }

  @SkailCommand()
  async EscalarEntregaNaoConfirmada(pedidoId: string, itemId: string): Promise<void> {
    await faturamento.EscalarEntregaNaoConfirmada(pedidoId, itemId);
  }

  @SkailCommand()
  async TratarDisputa(pedidoId: string, itemId: string, motivo: string): Promise<void> {
    await faturamento.TratarDisputa(pedidoId, itemId, motivo);
  }

  @SkailCommand()
  async LiberarPagamentoParaSeller(sellerId: string, itemId: string, valor: number): Promise<void> {
    await faturamento.LiberarPagamentoParaSeller(sellerId, itemId, valor);
  }

}
```

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

O `ProcessarPedido` dispara um fluxo independente para cada item (chamando um `ProcessarRepasseItem` pra cada) e só termina quando todos estiverem concluídos: um item pode levar dois dias, outro pode levar semanas.

Cada `ProcessarRepasseItem` segue seu próprio caminho, sem depender dos outros.\
\
A única coordenação necessária é a espera por todos os itens no topo, que o skail gerencia automaticamente.

### Cobrança recorrente de assinatura

Uma assinatura é, essencialmente, um ciclo:

* cobrar
* esperar X dias
* repetir até o cliente cancelar

No skail isso pode ser literalmente um `while` simples:

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

```csharp
[SkailFunction]
public async SkailTask AssinaturaMensal(Guid assinaturaId)
{
    var cancelamento = SkailTask.WaitForEvent<MotivoCancelamento>(
        "ASSINATURA_CANCELADA",
        assinaturaId.ToString()
    );

    while (!cancelamento.IsCompleted)
    {
        var faturaId = await EmitirFatura(assinaturaId);
        await CobrarCartao(faturaId);

        var proximoCiclo = SkailTask.Delay(TimeSpan.FromDays(30));

        await SkailTask.WhenAny(cancelamento, proximoCiclo);
    }

    var motivo = await cancelamento;
    await EncerrarAssinatura(assinaturaId, motivo);
}
```

{% endcode %}
{% endtab %}

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

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";
import {
  faturamento,
  type MotivoCancelamento,
  type ResultadoCobranca,
  type TentativaCobranca,
} from "./faturamento-integracoes.js";

const DiaMs = 24 * 60 * 60_000;

export class Assinaturas {
  @SkailFunction()
  async AssinaturaMensal(assinaturaId: string): Promise<void> {
    const cancelamento = Skail.waitForEvent<MotivoCancelamento>("ASSINATURA_CANCELADA", assinaturaId);

    let motivo: MotivoCancelamento | undefined;
    while (motivo === undefined) {
      const faturaId = await this.EmitirFatura(assinaturaId);
      await this.CobrarCartao(faturaId);

      const proximoCiclo = Skail.delay(30 * DiaMs);

      const cancelado = await Skail.whenAny(cancelamento, proximoCiclo);
      if (cancelado !== undefined) motivo = cancelado;
    }

    await this.EncerrarAssinatura(assinaturaId, motivo);
  }

  @SkailFunction()
  async CobrarCartao(faturaId: string): Promise<ResultadoCobranca> {
    const backoff = [
      0,
      5 * 60_000,
      60 * 60_000,
      6 * 60 * 60_000,
    ];

    for (const espera of backoff) {
      await Skail.delay(espera);

      const resultado = await this.TentarCobranca(faturaId);

      if (resultado.Sucesso) {
        await this.ConfirmarPagamento(faturaId, resultado.TransacaoId);
        return "Sucesso";
      }

      if (resultado.ErroPermanente) {
        await this.MarcarComoRecusada(faturaId, resultado.Motivo);
        return "Recusada";
      }
    }

    await this.EscalarParaCobrancaManual(faturaId);
    return "Pendente";
  }

  @SkailCommand()
  async EmitirFatura(assinaturaId: string): Promise<string> {
    return faturamento.EmitirFatura(assinaturaId);
  }

  @SkailCommand()
  async EncerrarAssinatura(assinaturaId: string, motivo: MotivoCancelamento): Promise<void> {
    await faturamento.EncerrarAssinatura(assinaturaId, motivo);
  }

  @SkailCommand()
  async NotificarFalhaNaCobranca(assinaturaId: string, faturaId: string): Promise<void> {
    await faturamento.NotificarFalhaNaCobranca(assinaturaId, faturaId);
  }

  @SkailCommand()
  async MarcarComoInadimplente(assinaturaId: string, faturaId: string): Promise<void> {
    await faturamento.MarcarComoInadimplente(assinaturaId, faturaId);
  }

  @SkailCommand()
  async TentarCobranca(faturaId: string): Promise<TentativaCobranca> {
    return faturamento.TentarCobranca(faturaId);
  }

  @SkailCommand()
  async ConfirmarPagamento(faturaId: string, transacaoId: string): Promise<void> {
    await faturamento.ConfirmarPagamento(faturaId, transacaoId);
  }

  @SkailCommand()
  async MarcarComoRecusada(faturaId: string, motivo: string): Promise<void> {
    await faturamento.MarcarComoRecusada(faturaId, motivo);
  }

  @SkailCommand()
  async EscalarParaCobrancaManual(faturaId: string): Promise<void> {
    await faturamento.EscalarParaCobrancaManual(faturaId);
  }
}
```

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

A execução dessa função pode durar todo o tempo da assinatura, meses ou até anos.

A cada ciclo ela:

* chama `EmitirFatura()` para gerar a próxima cobrança
* reutiliza o `CobrarCartao()` (com a política de retentativa mostrada anteriormente)
* e aguarda (hibernada) o próximo ciclo com um delay de 30 dias.

Se o evento de cancelamento (`ASSINATURA_CANCELADA`) for disparado a qualquer momento, a espera pelo que vier primeiro, o cancelamento ou o próximo ciclo, termina na hora ou, se a execução estava no meio de uma cobrança, assim que ela volta ao `while`; o loop termina e a assinatura é encerrada.

Por fim, toda a lógica de:

* quando é o próximo ciclo
* se a assinatura ainda está ativa
* quantas cobranças já foram feitas

fica implícita no próprio estado da execução (podendo ser acompanhado pelo [Monitor](/referencia/portal/monitor.md) do skail).

Você não precisa mais manter tabela no banco só para o scheduler saber o que fazer amanhã.\
\ <mark style="background-color:$success;">Ganho imediato: seu banco de dados fica muito mais leve, e é bem menos requisitado.</mark>

#### Assinatura com período de graça / Recuperação de Pagamento

A versão acima ignora o resultado da cobrança. O padrão mais comum é dar ao cliente uma janela para atualizar o meio de pagamento:

Se ele atualizar a tempo, o skail tenta cobrar de novo;\
Se não atualizar, a assinatura é marcada como inadimplente e encerrada.

{% hint style="warning" %}
Esta versão substitui a `AssinaturaMensal` anterior. Se já houver assinaturas em andamento na versão antiga, publique esta como nova versão do workload e mantenha a antiga publicada até essas execuções terminarem: cada execução é retomada pelo código da versão em que começou.
{% endhint %}

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

```csharp
[SkailFunction]
public async SkailTask AssinaturaMensal(Guid assinaturaId)
{
    var cancelamento = SkailTask.WaitForEvent<MotivoCancelamento>(
        "ASSINATURA_CANCELADA",
        assinaturaId.ToString()
    );

    while (!cancelamento.IsCompleted)
    {
        var faturaId = await EmitirFatura(assinaturaId);
        var resultado = await CobrarCartao(faturaId);

        if (resultado != ResultadoCobranca.Sucesso)
        {
            // Período de graça: o cliente tem 7 dias para atualizar o cartão.
            await NotificarFalhaNaCobranca(assinaturaId, faturaId);

            var cartaoAtualizado = SkailTask.WaitForEvent("CARTAO_ATUALIZADO", assinaturaId.ToString());
            var carencia = SkailTask.Delay(TimeSpan.FromDays(7));

            await SkailTask.WhenAny(cartaoAtualizado, carencia, cancelamento);

            if (cancelamento.IsCompleted) break;

            var cobrouDeNovo = cartaoAtualizado.IsCompleted
                && await CobrarCartao(faturaId) == ResultadoCobranca.Sucesso;

            if (!cobrouDeNovo)
            {
                await MarcarComoInadimplente(assinaturaId, faturaId);
                await EncerrarAssinatura(assinaturaId, MotivoCancelamento.Inadimplencia);
                return;
            }
        }

        var proximoCiclo = SkailTask.Delay(TimeSpan.FromDays(30));
        await SkailTask.WhenAny(cancelamento, proximoCiclo);
    }

    var motivo = await cancelamento;
    await EncerrarAssinatura(assinaturaId, motivo);
}
```

{% endcode %}
{% endtab %}

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

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";
import {
  faturamento,
  type MotivoCancelamento,
  type ResultadoCobranca,
  type TentativaCobranca,
} from "./faturamento-integracoes.js";

const DiaMs = 24 * 60 * 60_000;

export interface CartaoAtualizado {
  AtualizadoEm?: string;
}

export class Assinaturas {
  @SkailFunction()
  async AssinaturaMensal(assinaturaId: string): Promise<void> {
    const cancelamento = Skail.waitForEvent<MotivoCancelamento>("ASSINATURA_CANCELADA", assinaturaId);

    let motivo: MotivoCancelamento | undefined;
    while (motivo === undefined) {
      const faturaId = await this.EmitirFatura(assinaturaId);
      const resultado = await this.CobrarCartao(faturaId);

      if (resultado !== "Sucesso") {
        // Período de graça: o cliente tem 7 dias para atualizar o cartão.
        await this.NotificarFalhaNaCobranca(assinaturaId, faturaId);

        const cartaoAtualizado = Skail.waitForEvent<CartaoAtualizado>("CARTAO_ATUALIZADO", assinaturaId);
        const carencia = Skail.delay(7 * DiaMs);

        const vencedor = await Skail.whenAny(cartaoAtualizado, carencia, cancelamento);

        if (typeof vencedor === "string") {
          motivo = vencedor;
          break;
        }

        const cobrouDeNovo =
          vencedor !== undefined && (await this.CobrarCartao(faturaId)) === "Sucesso";

        if (!cobrouDeNovo) {
          await this.MarcarComoInadimplente(assinaturaId, faturaId);
          await this.EncerrarAssinatura(assinaturaId, "Inadimplencia");
          return;
        }
      }

      const proximoCiclo = Skail.delay(30 * DiaMs);
      const cancelado = await Skail.whenAny(cancelamento, proximoCiclo);
      if (cancelado !== undefined) motivo = cancelado;
    }

    await this.EncerrarAssinatura(assinaturaId, motivo);
  }

  @SkailFunction()
  async CobrarCartao(faturaId: string): Promise<ResultadoCobranca> {
    const backoff = [
      0,
      5 * 60_000,
      60 * 60_000,
      6 * 60 * 60_000,
    ];

    for (const espera of backoff) {
      await Skail.delay(espera);

      const resultado = await this.TentarCobranca(faturaId);

      if (resultado.Sucesso) {
        await this.ConfirmarPagamento(faturaId, resultado.TransacaoId);
        return "Sucesso";
      }

      if (resultado.ErroPermanente) {
        await this.MarcarComoRecusada(faturaId, resultado.Motivo);
        return "Recusada";
      }
    }

    await this.EscalarParaCobrancaManual(faturaId);
    return "Pendente";
  }

  @SkailCommand()
  async EmitirFatura(assinaturaId: string): Promise<string> {
    return faturamento.EmitirFatura(assinaturaId);
  }

  @SkailCommand()
  async EncerrarAssinatura(assinaturaId: string, motivo: MotivoCancelamento): Promise<void> {
    await faturamento.EncerrarAssinatura(assinaturaId, motivo);
  }

  @SkailCommand()
  async NotificarFalhaNaCobranca(assinaturaId: string, faturaId: string): Promise<void> {
    await faturamento.NotificarFalhaNaCobranca(assinaturaId, faturaId);
  }

  @SkailCommand()
  async MarcarComoInadimplente(assinaturaId: string, faturaId: string): Promise<void> {
    await faturamento.MarcarComoInadimplente(assinaturaId, faturaId);
  }

  @SkailCommand()
  async TentarCobranca(faturaId: string): Promise<TentativaCobranca> {
    return faturamento.TentarCobranca(faturaId);
  }

  @SkailCommand()
  async ConfirmarPagamento(faturaId: string, transacaoId: string): Promise<void> {
    await faturamento.ConfirmarPagamento(faturaId, transacaoId);
  }

  @SkailCommand()
  async MarcarComoRecusada(faturaId: string, motivo: string): Promise<void> {
    await faturamento.MarcarComoRecusada(faturaId, motivo);
  }

  @SkailCommand()
  async EscalarParaCobrancaManual(faturaId: string): Promise<void> {
    await faturamento.EscalarParaCobrancaManual(faturaId);
  }
}
```

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

Se a cobrança não deu certo:<br>

1. o cliente é notificado (`NotificarFalhaNaCobranca`)
2. o sistema entra em um período de graça de 7 dias (delay)
3. ao mesmo tempo, aguarda três possibilidades:
   * o cliente atualizar o cartão (espera por evento)
   * o prazo de 7 dias terminar
   * a assinatura ser cancelada

\
O fluxo reage ao que acontecer primeiro.<br>

* Se o cliente cancelar nesse meio-tempo, o `while` termina e a assinatura é encerrada com o motivo do cancelamento
* Se o cartão for atualizado dentro do prazo, o sistema tenta cobrar novamente (`CobrarCartao`)
* Se essa tentativa falhar, ou se o prazo terminar sem atualização, a assinatura é marcada como inadimplente e encerrada

A espera pelo cancelamento é a mesma criada no início da função. Por isso o período de graça fica dentro da `AssinaturaMensal`, e não em uma function separada: uma function não recebe uma espera em andamento como argumento, e criar uma segunda espera com o mesmo nome e id faria o fire do cancelamento atender só uma das duas.

<mark style="background-color:$success;">E tudo isso acontece sem a lógica ficar espalhada pelo sistema.</mark>

<mark style="background-color:$success;">Agora você mantém um código linear simples que mostra a sequência de etapas do processo para qualquer dev que precisa mantê-lo.</mark>

### Observabilidade

No Monitor do portal do skail, cada fatura em acompanhamento aparece como uma execução ativa, aguardando pagamento ou a próxima etapa.

\
Você consegue ver, por exemplo:

* quanto tempo os pagamentos estão levando
* quais faturas ainda estão em aberto aguardando
* e quais estão paradas há mais tempo

\ <mark style="background-color:$success;">Tudo isso vem do próprio estado das execuções, sem precisar montar dashboards ou cruzar dados em outros sistemas.</mark>
