> For the complete documentation index, see [llms.txt](https://docs.skail.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.skail.dev/aprender/exemplos-completos/faturamento-completo-cobranca-recorrencia-e-split-de-pagamento.md).

# Faturamento completo: 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.

A distância entre "o que quero fazer" e "o que preciso codar" costuma incomodar.

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)

Primeiro cenário:

Você tenta cobrar o cliente algumas vezes antes de desistir.

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

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 `[SkailCommand]`. 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.

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

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

Repare que a leitura do relógio (`ObterDataAtual`) está em um `[SkailCommand]`, e não direto na função. O corpo de uma `[SkailFunction]` é reexecutado a cada retomada e precisa produzir sempre o mesmo resultado; `DateTime.UtcNow` chamado ali quebraria essa regra. Dentro do command, o valor é lido uma vez e gravado.

#### Como isso funciona na prática

[`WaitForEvent`](/construir/sdk-.net/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;">Sem crontab, sem job scheduler, sem tabela de "próxima ação a tomar".</mark>

O método descreve a jornada inteira 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](/construir/api-http/post-api-v1-fire-eventname-instanceid.md). Ele não grava nada:

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

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 correndo, 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:

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

<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 (usando [`SkailTask.Delay`](/construir/sdk-.net/delay.md)) para o cliente ou o time decidirem. Depois, usa um [`WaitForEvent`](/construir/sdk-.net/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 segue seu próprio calendário e jornada, mas o pedido só é considerado concluído quando tudo termina.

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

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

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 é o `WhenAll` 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:

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

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 `SkailTask.Delay(TimeSpan.FromDays(30))`.

Se o evento de cancelamento (`ASSINATURA_CANCELADA`) for disparado a qualquer momento, a espera do `WhenAny()` 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](/construir/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.

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.

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

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 (`WaitForEvent`)
   * o prazo de 7 dias terminar
   * a assinatura ser cancelada

\
O `WhenAny` garante que 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 `SkailTask` 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 'conta a história' do seu sistema 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 o próximo passo da jornada.

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