Bugs na expiração do trial gratuito: casos-limite e regras claras
Bugs de expiração de trials gratuitos geralmente surgem de contas de tempo e estados pouco claros. Aprenda regras claras, casos de borda e testes para períodos de graça e comportamento pós-trial.

Por que trials gratuitos terminam no momento errado
Trials gratuitos frequentemente quebram de formas que parecem pessoais: um usuário é bloqueado um dia antes, é cobrado mais cedo do que esperava ou vê um banner “trial expirado” enquanto o app ainda o deixa entrar. Os tickets de suporte aparecem rápido: “Seu temporizador está errado”, “Eu me inscrevi ontem à noite” ou “Terminou durante minha demo”.
Isso acontece porque o tempo é bagunçado. Pessoas se cadastram em fusos diferentes. O horário de verão altera os relógios. Servidores e bancos de dados armazenam timestamps em formatos diferentes. Mesmo “30 dias” pode significar coisas diferentes se uma parte do código conta dias do calendário e outra conta horas. Some retries, jobs em background e caching, e você acaba com vários lugares calculando “o trial está ativo?” de formas levemente diferentes.
Uma fonte comum de confusão é misturar dois momentos separados:
Trial ended: o período gratuito acabou (ponto de decisão de cobrança).Access revoked: o usuário não pode mais usar funcionalidades pagas (ponto de decisão de produto).
Esses momentos podem coincidir, mas não precisam. Se você tem um período de graça, tentativas de cobrança ou overrides manuais, torne-os regras explícitas, não efeitos colaterais acidentais.
O objetivo é simples: defina uma única fonte de verdade para o tempo do trial (normalmente um timestamp armazenado trial_ends_at) e construa testes repetíveis em cima dela. Os testes devem incluir datas complicadas como fim de mês e mudanças de DST, não apenas “agora mais 7 dias”.
Os dados que você precisa rastrear (e o que não confiar)
A maioria dos bugs de expiração começa com dados faltantes ou vagos. Se você só armazena “trial = true” e um número de “trial days”, cada decisão posterior vira um palpite.
Armazene um pequeno conjunto de timestamps que permitam responder a uma pergunta: o que o usuário pode fazer agora?
No mínimo, mantenha:
trial_started_at: quando o trial realmente começou (após signup bem-sucedido, não após o carregamento da página)trial_ends_at: o momento exato em que o acesso ao trial paracanceled_at: quando o usuário cancelou (se você permitir cancelar durante o trial)paid_at: quando o primeiro pagamento teve sucesso (não quando você criou uma sessão de checkout)ended_at: quando você marcou o trial como encerrado (útil para auditoria e replays)
Salve esses valores como timestamps completos em UTC. UTC remove surpresas do horário de verão e evita bugs de “meia-noite” quando um usuário viaja ou um servidor roda em outra região. Converta para horário local apenas para exibição.
Não confie no tempo do dispositivo para aplicar regras. Telefones podem estar errados, usuários podem mudar o relógio e o tempo do navegador pode derivar. Use o tempo do servidor (ou do banco) como a fonte de “agora” e baseie comparações nesse único relógio.
Também evite misturar campos “apenas data” com timestamps reais. “2026-01-20” soa simples, mas esconde um fuso horário implícito e um corte que você não definiu. É meia-noite no local do usuário, meia-noite em UTC ou fim do dia? Essa ambiguidade é causa frequente de bugs de expiração.
Regra simples: se o acesso depende disso, armazene um timestamp UTC exato.
Regras de cálculo de tempo que evitam erros de um por um
A maioria dos bugs de expiração acontece porque o time de produto quer uma coisa (“duas semanas”) e o código faz outra (“até a próxima meia-noite”). Conserte escrevendo uma regra clara, e fazendo todas as telas e jobs usarem essa mesma regra.
Primeiro, decida o que é um trial.
Se você quer o comportamento mais simples e previsível, use uma duração fixa: 14 x 24 horas a partir de trial_started_at. Isso evita debates sobre o que “meia-noite” significa.
Se você realmente precisa de um trial baseado em data (“válido até uma data específica do calendário”), isso pode funcionar, mas somente se você definir o fuso horário desde o início (fuso do cliente ou um fuso único de cobrança) e nunca chutar.
Em seguida, defina a fronteira como exclusiva, não inclusiva. Uma regra limpa é: o trial é válido enquanto now < trial_ends_at. No instante now == trial_ends_at, o trial acabou. Isso elimina casos de borda onde dois sistemas discordam por um segundo.
Tenha cuidado com arredondamentos. Bugs aparecem quando um lugar armazena segundos, outro trunca para minutos e um terceiro mostra dias. Trate timestamps como precisos e arredonde apenas para exibição.
Por fim, documente o que “meia-noite” significa para o seu negócio. Se você usar meia-noite local em algum lugar, deve definir qual localidade e como mudanças de DST a afetam. Muitas equipes evitam isso fazendo todo o armazenamento e comparações em UTC.
Períodos de graça (grace): escolha uma política simples e mantenha-a
Um período de graça é um buffer depois que o acesso normalmente terminaria. Ele ajuda a reduzir bloqueios acidentais e dá tempo para sistemas de cobrança tentarem novamente sem gerar uma onda de tickets de suporte.
Onde as equipes se complicam é em rodar acidentalmente dois períodos de graça diferentes ao mesmo tempo. Escolha um gatilho e faça dele o único.
Escolha um tempo de início
Escolha uma das opções:
- Iniciar a grace em
trial_ends_at. - Iniciar a grace após a primeira tentativa de pagamento falhada.
Então escreva a regra em uma frase e mantenha-a estável, por exemplo: “Grace começa em trial_ends_at e dura 72 horas.” Armazene a duração (ou o grace_end calculado) para que mudar a política depois não reescreva o histórico.
Decida o que significa acesso durante a grace
Evite um acesso “meio que funciona”. Escolha um modo claro que produto e suporte possam explicar. Acesso total é mais simples, mas arriscado. Somente leitura é comum para dashboards. Um modo limitado (por exemplo, ver permitido mas exportar bloqueado) é muitas vezes um bom compromisso.
Overrides do suporte são outro ponto onde políticas silenciosamente quebram. Se você permitir extensões, trate-as como eventos explícitos: quem aprovou, por quanto tempo, por quê e qual é o novo horário de término. Registre isso junto dos eventos de cobrança para que auditorias e testes possam reproduzir.
Estados de trial-encerrado: explicite-os, não os infira
Muitos bugs acontecem quando “trial ended” é tratado como um palpite: se now > trial_ends_at, o sistema assume que a conta não está mais em trial. Isso funciona até você adicionar períodos de graça, pagamentos falhados, upgrades, reembolsos ou correções manuais.
Em vez disso, armazene um estado de assinatura explícito que seu sistema possa raciocinar. Mantenha-o pequeno e sem graça:
trialingactivepast_duecanceledexpired
Então defina transições permitidas e o que as dispara. Por exemplo:
trialing -> activequando o primeiro pagamento for bem-sucedidotrialing -> expiredquandotrial_ends_atfor atingido e não houver conversãoactive -> past_duequando uma renovação falharpast_due -> activequando o pagamento for bem-sucedido posteriormenteactive -> canceledquando o usuário cancelar
A reativação é onde lógica implícita cria sobras estranhas. Se você permitir expired -> active (upgrade pós-trial), trate isso como um recomeço limpo: defina state=active, registre um novo fim de período pago (por exemplo current_period_ends_at) e limpe ou ignore campos só do trial. Caso contrário, você acaba com usuários “active” ainda sujeitos a bloqueios de trial.
Por fim, faça um único lugar no código decidir acesso baseado em state + timestamps. Não espalhe checagens de acesso por controllers, UI e jobs em background. Mantenha uma função de política única que todas as superfícies chamem.
Casos de borda que quebram a lógica de expiração
A maioria dos bugs aparece quando o tempo real fica bagunçado. Algumas regras cobrem quase todos os casos, desde que você as aplique consistentemente.
- Armazene todos os timestamps de trial em UTC e trate-os como fonte de verdade. Converta para o fuso do usuário apenas para exibição.
- Não converta de volta e para frente durante cálculos. É assim que um trial “anda” uma hora dependendo de onde o código roda.
O horário de verão é a armadilha clássica. Um “trial de 14 dias” nem sempre é o mesmo que “336 horas” quando você envolve tempo local. Decida o que você promete e codifique: ou “expira após N x 24 horas em UTC” ou “expira no mesmo horário local após N dias do calendário”. Escolha um e teste os finais de semana de DST.
Finais de mês e dias bissextos causam surpresas semelhantes quando as pessoas misturam matemática de calendário com matemática de duração. “Adicionar 1 mês” a 31 de janeiro pode virar 28/29 de fevereiro, depois 28/29 de março, o que parece errado se você esperava “fim do mês”. Se seu trial é medido em dias, evite meses.
Questões operacionais também quebram expiração:
- Deslocamento de relógio: nunca use o tempo do dispositivo para aplicação.
- Deriva do servidor: confie em uma fonte de tempo única para todos os serviços.
- Jobs atrasados: tarefas enfileiradas podem rodar tarde, então checagens devem ser idempotentes.
- Retries: eventos duplicados de “trial ended” não devem cobrar em dobro.
Cancelamento, upgrades e mudanças de plano durante um trial
A lógica de expiração costuma quebrar quando regras “normais” colidem com ações do usuário. Se você não definir regras para cancelamento e mudanças de plano, o app vai chutar e diferentes telas chutarão de maneiras distintas.
Se um usuário cancelar durante o trial
Escolha uma política e torne-a explícita:
- Cancelar agora, manter acesso até
trial_ends_at. - Cancelar agora, encerrar acesso imediatamente.
“Fim do dia” soa simples, mas tende a criar brigas de fuso horário. Se você usar isso, deve definir qual fuso e o que “fim do dia” significa.
Qualquer que seja a escolha, armazene como dado. Por exemplo: mantenha trial_ends_at inalterado, defina canceled_at, marque will_renew=false e deixe checagens de acesso lerem esses campos. Adicione um teste que cancele 1 minuto antes do fim do trial e confirme o mesmo resultado na API e na UI.
Upgrades, downgrades e mudanças de plano
Upgrades são onde dinheiro e tempo se encontram, então decida se um upgrade gera cobrança imediata ou espera o fim do trial.
Um conjunto de regras limpo parece com isto:
- Upgrade durante o trial: ou cobre imediatamente e define
trial_ends_at=null, ou agenda o plano pago para começar emtrial_ends_at. - Downgrade durante o trial: não zere o relógio do trial; apenas mude o que acontece após o trial.
- Troca de planos: não recompute
trial_ends_ata partir de “agora” a menos que você intencionalmente esteja concedendo mais tempo.
Se você cobrar ao fim do trial, espere reembolsos e chargebacks. A abordagem mais segura é separar “trial ended” de “pagamento sucedido”. Um trial pode acabar, um pagamento pode falhar e as regras de acesso devem lidar com isso sem jogar usuários em estados aleatórios.
Passo a passo: implemente expiração com uma fonte de verdade
Diferentes partes de um app frequentemente “decidem” o status do trial independentemente: uma página checa um timestamp, um webhook checa outro e a cobrança usa uma regra terceira. Conserte isso escrevendo as regras uma vez e fazendo tudo chamar a mesma decisão.
Comece escrevendo a política em inglês claro com timestamps concretos, fusos e resultados. Exemplo: “Um trial de 7 dias que começa 2026-01-20 10:15:00Z termina em 2026-01-27 10:15:00Z. O acesso é permitido até o instante exato de término, então negado.” Se permitir uma grace, especifique-a com a mesma precisão.
Uma sequência prática:
- Defina a política com alguns exemplos reais (inclua um próximo à meia-noite e outro durante mudança de DST).
- Implemente uma função de decisão que retorne
can_accessmais uma string de razão. - Quando o trial for criado, compute e armazene um único
trial_ends_atem UTC. Não recalculá-lo em views, webhooks ou jobs. - Faça mudanças de estado em um único lugar: ou um job agendado marca trials como encerrados, ou uma checagem on-request atualiza o estado quando o usuário carrega o app. Escolha uma abordagem e mantenha-a.
- Registre cada decisão com inputs e output (id do usuário,
now, timestamps armazenados, decisão). Isso acelera disputas e debugging.
Erros comuns que criam bugs de expiração
A maioria dos bugs de expiração não são “problemas de matemática”. Eles vêm de ter mais de um lugar decidindo se um usuário deve ter acesso.
Timers no cliente são uma falha clássica. Se a UI usa o relógio do dispositivo, usuários podem mudar o tempo, cruzar fusos ou sofrer shifts de DST e ganhar algumas horas (ou perdê-las). A aplicação pertence ao servidor, usando uma única fonte de tempo.
Lógica dividida é igualmente dolorosa: a UI diz “trial ativo” enquanto a API bloqueia requisições, ou a API permite acesso enquanto a UI mostra uma parede de pagamento. Isso geralmente acontece quando checagens foram adicionadas em pontos diferentes ao longo do tempo.
Outros problemas comuns:
- Comparar datas como strings ("2026-1-2" vs "2026-01-02")
- Misturar milissegundos e segundos entre serviços
- Arredondar timestamps para meia-noite sem concordar num fuso horário
- Tratar
trial_ends_atcomo inclusivo em um lugar e exclusivo em outro
Cuidado também com extensões acidentais de trial. É fácil resetar trial_ends_at no login, refresh ou visitas à página de preços, especialmente quando a configuração do trial é disparada de múltiplas telas.
Webhooks acrescentam sua própria bagunça. Eventos de pagamento podem chegar atrasados, reenviados ou fora de ordem. Faça handlers idempotentes, armazene o último tempo de evento processado e baseie o acesso no estado de assinatura armazenado, não “no webhook que chegou por último”.
Checklist rápido antes de liberar
A maioria dos bugs aparece somente após o lançamento, quando usuários reais atingem padrões estranhos de tempo e retries. Antes de liberar, confirme que isto é verdade no seu código e nos testes:
trial_ends_atestá armazenado em UTC e não muda depois da criação a menos que você o estenda explicitamente.- Toda comparação usa a mesma regra em todos os lugares (por exemplo: acesso permitido enquanto
now < trial_ends_at). - O comportamento de grace é consistente entre UI, API e cobrança.
- Eventos atrasados não fazem o estado retroceder.
- Logs mostram os timestamps exatos usados para cada decisão de acesso (
now, tempos armazenados, estado resultante).
Um cenário de sanidade rápido
Crie um usuário de teste cujo trial termina em 2026-01-20T00:00:00Z. Verifique acesso 1 segundo antes, exatamente no instante e 1 segundo depois. Repita por cada caminho que pode restringir acesso: requisição API, job agendado de expiração e qualquer handler de webhook.
Cenário de exemplo e próximos passos
Percorra uma linha do tempo confusa e escreva o que o usuário deve ver em cada passo.
Um usuário se cadastra na sexta às 23:30 em uma região onde o DST começa no domingo (os relógios adiantam). No cadastro, defina trial_started_at com o timestamp exato do signup e trial_ends_at = trial_started_at + duration.
Sábado: o usuário está trialing e tem acesso. A UI deve mostrar tempo restante com base em trial_ends_at, não atalhos por dias do calendário.
Domingo: o DST muda. O usuário continua trialing. Nada muda no backend. Só a exibição muda.
Segunda 23:35: o usuário abre o app e tenta pagar.
Se você oferece 24 horas de grace após o fim do trial, o comportamento esperado deve ser o mesmo em todos os lugares:
- Até
trial_ends_at: acesso de trial - De
trial_ends_atatétrial_ends_at + grace: acesso em grace (o que você documentou) - Após o término da grace sem pagamento:
expirede bloqueado (exceto para cobrança) - Após um pagamento bem-sucedido a qualquer momento:
active
Os testes para esse cenário devem existir antes do lançamento: fim de trial durante uma mudança de DST, checagens de fronteira (1 segundo antes/na/1 segundo depois), caminhos de sucesso e falha de pagamento e concordância UI/API sobre o estado.
Se sua lógica de assinaturas foi gerada rapidamente e agora se comporta de forma inconsistente, uma auditoria curta frequentemente encontra a causa raiz: fusos misturados, checagens de acesso espalhadas ou múltiplas definições concorrentes de “trial ativo”. FixMyMess (fixmymess.ai) foca em transformar protótipos gerados por IA em sistemas prontos para produção, consolidando lógica de tempo e estado em um único ponto de decisão testado.
Perguntas Frequentes
Por que o teste gratuito de um usuário terminou um dia antes?
Isso geralmente é uma discrepância de fuso horário ou arredondamento. Uma parte do sistema pode estar contando “dias do calendário” (terminando na meia-noite) enquanto outra conta “horas desde o cadastro”, então usuários próximos à meia-noite perdem quase um dia inteiro. Corrija armazenando um único timestamp trial_ends_at em UTC e aplicando a mesma comparação de acesso em todos os lugares.
Quais timestamps devo armazenar para evitar bugs de expiração de trial?
Armazene timestamps completos em UTC que permitam responder às perguntas de acesso sem adivinhação: trial_started_at, trial_ends_at, paid_at, canceled_at e um campo de auditoria como ended_at se precisar registrar quando marcou como encerrado. Evite depender de um booleano como trial=true mais um trial_days, porque cada verificação posterior vai recalcular de forma diferente.
Como devo calcular `trial_ends_at` para um trial de 7 ou 14 dias?
Calcule-o uma vez no momento em que o trial realmente começa (após o cadastro bem-sucedido, não após o carregamento da página). Para trials por duração, defina trial_ends_at = trial_started_at + (N * 24 horas) em UTC e persista esse valor. Não recalcule na UI, webhooks ou jobs em background.
`trial_ends_at` deve ser inclusivo ou exclusivo?
Use uma fronteira exclusiva: permita acesso enquanto now < trial_ends_at. No instante em que now == trial_ends_at, o trial terminou. Isso remove divergências de “mesmo segundo” entre serviços e evita casos de borda onde um sistema considera o instante final ainda válido.
Posso aplicar a expiração do trial usando o tempo do navegador ou do telefone?
Faça a aplicação no servidor usando um relógio confiável (tempo do servidor ou do banco). Relógios de dispositivos variam, usuários podem alterá-los e navegadores podem estar em fuso diferente do backend. Você pode mostrar uma contagem regressiva no cliente, mas a API deve ser a autoridade final.
Como adiciono um período de graça sem criar regras confusas de acesso?
Escolha uma política simples e escreva-a em uma frase, depois implemente em um único lugar. Um padrão comum: “A carência (grace) começa em trial_ends_at e dura 72 horas”, com acesso definido claramente durante esse período (acesso total, somente leitura ou modo limitado). Armazene a regra de grace ou o tempo derivado de término para que mudanças futuras não reescrevam contas antigas.
Qual é a política de cancelamento mais segura durante um trial?
Escolha uma regra e torne-a consistente entre UI e API. A opção menos surpreendente é “cancelar agora, manter acesso até trial_ends_at”, definindo canceled_at e will_renew=false (ou equivalente) para que a cobrança não comece. Se você encerrar o acesso imediatamente ao cancelar, deixe isso explícito e teste cancelamentos perto do fim do trial.
Como lido com webhooks de cobrança atrasados ou duplicados sem quebrar o acesso?
Trate webhooks de forma idempotente e baseada em estado. Eventos de pagamento podem chegar atrasados, ser reenviados ou aparecer fora de ordem, então não deixe “o último webhook que chegou” decidir o acesso. Armazene o estado da assinatura (trialing, active, past_due, expired, canceled) mais timestamps, e permita que webhooks atualizem esse estado apenas quando a transição for válida.
Quais testes pegam os casos de borda mais comuns de expiração de trial?
Teste fronteiras e datas estranhas, não apenas “agora + 7 dias”. Adicione testes para 1 segundo antes/na/data/depois de trial_ends_at, finais de mês, dia bissexto e os finais de semana de DST nos fusos relevantes. Também teste que UI e API concordam sobre o mesmo estado de conta para os mesmos timestamps armazenados.
Quando devo chamar a FixMyMess para consertar lógica de trials e assinaturas?
Quando você percebe comportamentos contraditórios — UI diz “trial ativo” mas a API bloqueia, endpoints calculam expiração de formas diferentes, ou trials mudam uma hora em torno do DST — é hora de auditar. FixMyMess (fixmymess.ai) pode rodar uma auditoria gratuita de código para localizar verificações de acesso espalhadas, unidades de tempo misturadas e vazamentos de fuso horário, e então consolidar tudo em um único ponto de decisão testado para que os trials terminem exatamente quando você pretende.