Errores en la expiración de pruebas gratuitas: casos límite y reglas claras
Los errores en la expiración de pruebas gratuitas suelen venir de cálculos de tiempo y estados poco claros. Aprende reglas claras, casos límite y pruebas para periodos de gracia y comportamiento tras el fin de la prueba.

Por qué las pruebas gratuitas terminan en el momento equivocado
Las pruebas gratuitas suelen fallar de formas que se sienten personales: un usuario queda bloqueado un día antes, se le cobra antes de lo esperado o ve un banner de “prueba expirada” mientras la app aún le deja entrar. Los tickets de soporte llegan rápido: “Su temporizador está mal”, “me registré anoche” o “terminó durante mi demo”.
Esto pasa porque el tiempo es complicado. La gente se registra en distintas zonas horarias. El horario de verano mueve los relojes. Los servidores y las bases de datos almacenan timestamps en formatos distintos. Incluso “30 días” puede significar cosas distintas si una parte del código cuenta días del calendario y otra cuenta horas. Suma reintentos, jobs en background y caché, y acabas con varios lugares calculando “¿la prueba está activa?” de formas ligeramente diferentes.
Una fuente común de confusión es mezclar dos momentos separados:
- Trial ended: el periodo gratuito terminó (punto de decisión de facturación).
- Access revoked: el usuario ya no puede usar funciones de pago (punto de decisión de producto).
Esos momentos pueden coincidir, pero no tienen por qué. Si tienes un periodo de gracia, reintentos o anulaciones manuales, hazlos reglas explícitas, no efectos secundarios accidentales.
El objetivo es simple: define una sola fuente de la verdad para el tiempo de la prueba (normalmente un timestamp almacenado trial_ends_at) y crea tests repetibles alrededor de ella. Los tests deben incluir fechas complicadas como finales de mes y cambios de DST, no solo “ahora más 7 días”.
Los datos que debes rastrear (y en qué no confiar)
La mayoría de los bugs de expiración empiezan con datos faltantes o difusos. Si solo guardas “trial = true” y un número de “trial days”, cada decisión posterior se convierte en una suposición.
Guarda un pequeño conjunto de timestamps que te permitan responder a una pregunta: ¿qué puede hacer el usuario ahora mismo?
Como mínimo, conserva:
trial_started_at: cuándo empezó la prueba realmente (después del registro exitoso, no después de cargar la página)trial_ends_at: el momento exacto en que el acceso de prueba se detienecanceled_at: cuándo canceló el usuario (si permites cancelar durante la prueba)paid_at: cuándo ocurrió el primer pago exitoso (no cuándo creaste una sesión de checkout)ended_at: cuándo marcaste la prueba como terminada (útil para auditorías y reproducciones)
Guarda estos como timestamps completos en UTC. UTC elimina sorpresas por horario de verano y evita bugs de “medianoche” cuando un usuario viaja o un servidor corre en otra región. Convierte a hora local solo para mostrarlo.
No confíes en la hora del dispositivo para la aplicación de reglas. Los móviles pueden estar mal, los usuarios pueden cambiar el reloj y la hora del navegador puede desviarse. Usa la hora del servidor (o de la base de datos) como la fuente de “ahora”, y basa las comparaciones en ese único reloj.
También evita mezclar campos “solo fecha” con timestamps reales. “2026-01-20” suena simple, pero oculta una zona horaria implícita y un corte que no has definido. ¿Es medianoche en la ubicación del usuario, medianoche UTC o fin de día? Esa ambigüedad es causa frecuente de bugs de expiración.
Regla simple: si el acceso depende de ello, almacena un timestamp UTC exacto.
Reglas de cálculo de tiempo que evitan errores off-by-one
La mayoría de los bugs de expiración ocurren porque el equipo de producto quiere una cosa (“dos semanas”) y el código hace otra (“hasta la siguiente medianoche”). Arréglalo escribiendo una regla clara y haciendo que cada pantalla y job la use igual.
Primero, decide qué es una prueba.
Si quieres el comportamiento más simple y predecible, usa una duración fija: 14 x 24 horas desde trial_started_at. Esto evita debates sobre qué significa “medianoche”.
Si realmente necesitas una prueba basada en fecha (“válida hasta una fecha de calendario específica”), puede funcionar, pero solo si defines la zona horaria desde el principio (zona horaria del cliente o una sola zona de facturación) y nunca adivinas.
A continuación, define el límite como exclusivo, no inclusivo. Una regla clara es: la prueba es válida mientras now < trial_ends_at. En el instante now == trial_ends_at, la prueba ha terminado. Eso elimina casos límite donde dos sistemas discrepan por un segundo.
Ten cuidado con el redondeo. Aparecen bugs cuando un lugar almacena segundos, otro trunca a minutos y un tercero muestra días. Trata los timestamps como precisos y redondea solo para mostrarlos.
Finalmente, documenta qué significa “medianoche” para tu negocio. Si usas medianoche local en algún sitio, debes definir qué localidad y cómo afectan los cambios por DST. Muchos equipos evitan eso haciendo todo el almacenamiento y las comparaciones en UTC.
Periodos de gracia: elige una política simple y cúmplela
Un periodo de gracia es un buffer después de que el acceso normalmente terminaría. Ayuda a reducir bloqueos accidentales y da tiempo a los sistemas de facturación para reintentar pagos sin generar una ola de tickets de soporte.
Donde los equipos suelen fallar es en ejecutar por accidente dos periodos de gracia distintos a la vez. Elige un único disparador y hazlo el único.
Elige un único momento de inicio
Escoge una de estas opciones:
- Iniciar la gracia en
trial_ends_at. - Iniciar la gracia después del primer intento de pago fallido.
Luego escribe la regla en una frase y mantenla estable, por ejemplo: “La gracia empieza en trial_ends_at y dura 72 horas.” Guarda la duración (o el grace_end calculado) para que cambiar la política más tarde no reescriba el historial.
Decide qué significa el acceso durante la gracia
Evita un acceso “medio funciona”. Elige un modo claro que producto y soporte puedan explicar. El acceso completo es lo más simple pero arriesgado. Modo solo lectura es común para dashboards. Un modo limitado (por ejemplo, ver contenido permitido pero exportar bloqueado) suele ser un buen compromiso.
Las anulaciones de soporte son otro lugar donde las políticas se rompen silenciosamente. Si permites extensiones, trátalas como eventos explícitos: quién lo aprobó, por cuánto tiempo, por qué y cuál es el nuevo tiempo de fin. Regístralo junto a los eventos de facturación para que auditorías y tests puedan reproducirlo.
Estados tras el fin de la prueba: hazlos explícitos, no supuestos
Muchos bugs ocurren cuando “trial ended” se trata como una suposición: si now > trial_ends_at, el sistema asume que la cuenta ya no está en prueba. Eso funciona hasta que añades periodos de gracia, pagos fallidos, upgrades, reembolsos o correcciones manuales.
En vez de eso, almacena un estado de suscripción explícito que tu sistema pueda razonar. Mantenlo pequeño y aburrido:
trialingactivepast_duecanceledexpired
Luego define transiciones permitidas y qué las dispara. Por ejemplo:
trialing->activecuando el primer pago tiene éxitotrialing->expiredcuando se alcanzatrial_ends_aty no hay conversiónactive->past_duecuando una renovación fallapast_due->activecuando un pago posterior tiene éxitoactive->canceledcuando el usuario cancela
La reactivación es donde la lógica implícita crea residuos extraños. Si permites expired -> active (upgrade después de la prueba), trátalo como un inicio limpio: establece state=active, registra un nuevo fin de periodo pagado (por ejemplo current_period_ends_at) y borra o ignora campos exclusivos de la prueba. De lo contrario acabarás con usuarios “active” que aún reciben bloqueos por haber terminado la prueba.
Finalmente, haz que un único lugar en el código decida el acceso con base en state + timestamps. No disperses comprobaciones en controladores, UI y jobs en background. Mantén una única función de política que todas las superficies llamen.
Casos límite que rompen la lógica de expiración
La mayoría de los bugs aparecen cuando el tiempo real se complica. Unas pocas reglas cubren casi todos los casos, siempre que las apliques consistentemente.
- Almacena todos los timestamps de prueba en UTC y trátalos como la fuente de la verdad. Convierte a la zona horaria del usuario solo para mostrar.
- No conviertas de ida y vuelta durante cálculos. Ahí es donde una prueba puede “moverse” una hora dependiendo de dónde se ejecute el código.
El horario de verano es la trampa clásica. Una “prueba de 14 días” no siempre equivale a “336 horas” si involucras la hora local. Decide qué prometes y codifícalo: o “expira después de N x 24 horas en UTC” o “expira a la misma hora local del reloj después de N días calendario”. Elige uno y prueba los fines de semana de DST.
Los finales de mes y los días bisiestos causan sorpresas similares cuando la gente mezcla cálculo por calendario con cálculo por duración. “Agregar 1 mes” desde el 31 de enero puede convertirse en 28/29 de febrero, luego 28/29 de marzo, lo cual se siente raro si esperabas “fin de mes”. Si tu prueba se mide en días, evita meses por completo.
Problemas operativos también pueden romper la expiración:
- Desfase de reloj: nunca uses la hora del dispositivo para la aplicación de reglas.
- Deriva de servidores: confía en una única fuente de tiempo para todos los servicios.
- Jobs tardíos: las tareas en cola pueden ejecutarse tarde, así que las comprobaciones deberían ser idempotentes.
- Reintentos: eventos duplicados de “prueba terminada” no deberían provocar cobros dobles.
Cancelaciones, upgrades y cambios de plan durante una prueba
La lógica de expiración suele romperse cuando las reglas “normales” chocan con acciones de usuarios. Si no defines reglas para cancelaciones y cambios de plan, la app adivinará y distintas pantallas adivinarán distinto.
Si un usuario cancela durante la prueba
Elige una política y hazla explícita:
- Cancelar ahora, mantener acceso hasta
trial_ends_at. - Cancelar ahora, finalizar acceso de inmediato.
“El fin del día” suena simple, pero suele provocar peleas por zonas horarias. Si lo usas, debes definir qué zona horaria y qué significa “fin del día”.
Sea lo que sea, guárdalo como datos. Por ejemplo: mantén trial_ends_at sin cambios, establece canceled_at, marca will_renew=false y deja que las comprobaciones de acceso lean esos campos. Añade un test que cancele 1 minuto antes del fin de la prueba y confirma el mismo resultado en API y UI.
Upgrades, downgrades y cambios de plan
Los upgrades son donde el dinero y el tiempo se encuentran, así que decide si un upgrade activa facturación inmediata o espera al final de la prueba.
Un conjunto de reglas limpio podría ser:
- Upgrade durante la prueba: o cobrarlas inmediatamente y fijar
trial_ends_at=null, o programar que el plan pagado comience entrial_ends_at. - Downgrade durante la prueba: no reinicies el reloj de la prueba; solo cambia lo que ocurre después de la prueba.
- Cambio de plan: no recalcules
trial_ends_atdesde “ahora” a menos que estés concediendo tiempo adicional intencionalmente.
Si cobras al final de la prueba, planea reembolsos y contracargos. El enfoque más seguro es separar “prueba terminada” de “pago exitoso”. Una prueba puede terminar, un pago puede fallar y las reglas de acceso deben manejar eso sin enviar a los usuarios a estados aleatorios.
Paso a paso: implementar expiración con una única fuente de la verdad
Diferentes partes de una app suelen “decidir” el estado de la prueba de forma independiente: una página comprueba un timestamp, un webhook otro y facturación usa una tercera regla. Arréglalo escribiendo las reglas una vez y haciendo que todo llame a la misma decisión.
Empieza escribiendo la política en lenguaje claro con timestamps concretos, zonas horarias y resultados. Ejemplo: “Una prueba de 7 días que empieza 2026-01-20 10:15:00Z termina el 2026-01-27 10:15:00Z. El acceso está permitido hasta el instante exacto de fin, luego se deniega.” Si permites un periodo de gracia, especifícalo con la misma precisión.
Una secuencia práctica:
- Define la política con algunos ejemplos reales (incluye uno cerca de medianoche y otro cerca de un cambio de DST).
- Implementa una función de decisión que devuelva
can_accessmás una cadena con la razón. - Cuando se crea la prueba, calcula y almacena un único valor
trial_ends_aten UTC. No lo recalcules en vistas, webhooks ni jobs. - Aplica los cambios de estado en un único lugar: o un job programado que marque pruebas como terminadas, o una comprobación al solicitar que actualice el estado cuando el usuario carga la app. Elige un enfoque y mantente con él.
- Registra cada decisión con entradas y salida (
user id,now, timestamps almacenados, decisión). Esto acelera disputas y debugging.
Errores comunes que crean bugs de expiración
La mayoría de los bugs de expiración no son “problemas de matemáticas”. Vienen de tener más de un lugar decidiendo si un usuario debe tener acceso.
Los temporizadores del lado cliente son un fallo clásico. Si la UI usa el reloj del dispositivo, los usuarios pueden cambiar la hora, cruzar zonas horarias o sufrir cambios de DST y ganar horas extra (o perderlas). La aplicación de reglas pertenece al servidor, usando una única fuente de tiempo.
La lógica dividida es igual de dolorosa: la UI dice “trial active” mientras la API bloquea peticiones, o la API permite acceso mientras la UI muestra un paywall. Esto suele ocurrir cuando las comprobaciones se añadieron en distintos momentos.
Otros problemas habituales:
- Comparar fechas como cadenas ("2026-1-2" vs "2026-01-02")
- Mezclar milisegundos y segundos entre servicios
- Redondear timestamps a medianoche sin acordar una zona horaria
- Tratar
trial_ends_atcomo inclusivo en un lugar y exclusivo en otro
Vigila extensiones accidentales de prueba también. Es fácil restablecer trial_ends_at al iniciar sesión, al refrescar o al visitar la página de precios, especialmente cuando la configuración de la prueba se ejecuta desde varias pantallas.
Los webhooks añaden su propio desorden. Los eventos de pago pueden llegar tarde, reintentarse o aparecer fuera de orden. Haz que los handlers sean idempotentes, guarda la hora del último evento procesado y basa el acceso en tu estado de suscripción almacenado, no en “el webhook que llegó último”.
Lista de verificación rápida antes de lanzar
La mayoría de los bugs aparecen solo después del lanzamiento, cuando usuarios reales golpean patrones raros de tiempo y reintentos. Antes de lanzar, confirma que esto sea cierto en tu código y en tus tests:
trial_ends_atestá almacenado en UTC y no cambia después de la creación salvo que lo extiendas explícitamente.- Cada comparación usa la misma regla en todas partes (por ejemplo: acceso permitido mientras
now < trial_ends_at). - El comportamiento de la gracia es consistente entre UI, API y la aplicación de facturación.
- Los eventos tardíos no revierten el estado.
- Los logs muestran los timestamps exactos usados para cada decisión de acceso (
now, tiempos de fin almacenados, estado resultante).
Un escenario de cordura rápido
Crea un usuario de prueba cuya prueba termine en 2026-01-20T00:00:00Z. Verifica acceso 1 segundo antes, exactamente en, y 1 segundo después. Repite por cada camino que puede condicionar acceso: petición API, job programado de expiración y cualquier handler de webhook.
Escenario de ejemplo y siguientes pasos
Recorre una línea de tiempo complicada y escribe qué debería ver el usuario en cada paso.
Un usuario se registra el viernes a las 23:30 en una región donde el DST comienza el domingo (los relojes adelantan). En el registro, fija trial_started_at al timestamp exacto de registro y calcula trial_ends_at = trial_started_at + duration.
Sábado: el usuario está trialing y tiene acceso. La UI debe mostrar tiempo restante basado en trial_ends_at, no en atajos de días calendario.
Domingo: ocurre el cambio de DST. El usuario sigue trialing. Nada cambia en el backend. Solo cambia la presentación.
Lunes 23:35: el usuario abre la app e intenta pagar.
Si ofreces un periodo de gracia de 24 horas después del fin de la prueba, el comportamiento esperado debe leerse igual en todas partes:
- Hasta
trial_ends_at: acceso de prueba - Desde
trial_ends_athastatrial_ends_at + grace: acceso en gracia (según lo documentado) - Tras finalizar la gracia sin pago:
expiredy bloqueo (excepto facturación) - Tras un pago exitoso en cualquier momento:
active
Los tests para este escenario deben existir antes de lanzar: fin de prueba sobre un cambio de DST, comprobaciones límite (un segundo antes/en/después), caminos de pago exitoso y fallido, y acuerdo UI/API sobre el estado.
Si tu lógica de suscripción se generó rápido y ahora se comporta de forma inconsistente, una auditoría breve suele encontrar la causa raíz: zonas horarias mezcladas, checks de acceso dispersos o múltiples definiciones competidoras de “trial active”. FixMyMess (fixmymess.ai) se centra en transformar prototipos generados por IA en sistemas listos para producción consolidando la lógica de tiempo y estado en un único punto de decisión probado.
Preguntas Frecuentes
Why did a user’s free trial end a day early?
Esto suele ser un desajuste de zona horaria o de redondeo. Una parte del sistema puede estar contando “días calendario” (terminando a medianoche) mientras otra cuenta “horas desde el registro”, de modo que los usuarios cerca de la medianoche pierden casi un día completo. Solución: guarda un único trial_ends_at en UTC y aplica la misma comparación en todos los lugares.
What timestamps should I store to avoid trial expiry bugs?
Guarda timestamps UTC completos que te permitan responder preguntas de acceso sin adivinar: trial_started_at, trial_ends_at, paid_at, canceled_at, y un campo de auditoría como ended_at si necesitas registrar cuándo marcaste el fin. Evita depender solo de un booleano como trial=true más “trial_days”, porque cada verificación posterior recalculará de forma distinta.
How should I compute `trial_ends_at` for a 7-day or 14-day trial?
Cálculoalo una vez en el momento en que la prueba realmente empieza (después del registro exitoso, no después de cargar la página). Para una prueba basada en duración, fija trial_ends_at = trial_started_at + (N * 24 hours) en UTC y persístelo. No lo vuelvas a calcular en la UI, webhooks ni jobs en background.
Should `trial_ends_at` be inclusive or exclusive?
Usa un límite exclusivo: permite acceso mientras now < trial_ends_at. El instante en que now == trial_ends_at, la prueba ha terminado. Esto elimina desacuerdos “mismo segundo” entre servicios y evita casos límite donde un sistema considera válido el momento final.
Can I enforce trial expiry using the browser or phone time?
Aplica la política en el servidor usando un reloj de confianza (tiempo del servidor o de la base de datos). Los relojes de los dispositivos se desvían, los usuarios pueden cambiarlos y los navegadores pueden estar en otra zona horaria que el backend. Puedes mostrar una cuenta regresiva en la UI, pero la API debe ser la autoridad final.
How do I add a grace period without creating confusing access rules?
Elige una política simple y escríbela en una frase, luego impleméntala en un único lugar. Un ejemplo común: “La gracia empieza en trial_ends_at y dura 72 horas”, con el acceso durante la gracia claramente definido (acceso completo, solo lectura o modo limitado). Guarda la regla de gracia o el tiempo derivado para que futuros cambios de política no reescriban cuentas antiguas.
What’s the safest cancellation policy during a trial?
Elige una regla y mantenla consistente entre UI y API. La opción menos sorprendente es “cancelar ahora, mantener acceso hasta trial_ends_at”, guardando canceled_at y will_renew=false (o equivalente) para que la facturación no comience. Si decides cortar el acceso inmediatamente al cancelar, hazlo explícito y prueba cancelaciones cerca del final de la prueba.
How do I handle late or duplicate billing webhooks without breaking access?
Haz que el manejo de webhooks sea idempotente y basado en estado. Los eventos de pago pueden llegar tarde, reintentarse o llegar fuera de orden, así que no dejes que “el último webhook gana” decida el acceso. En su lugar, guarda el estado de suscripción (trialing, active, past_due, expired, canceled) y timestamps, y permite que los webhooks actualicen ese estado solo cuando la transición sea válida.
What tests catch the most common trial expiry edge cases?
Prueba los límites y las fechas raras, no solo “ahora + 7 días”. Añade tests para un segundo antes/en/el segundo después de trial_ends_at, finales de mes, años bisiestos y fines de semana de DST en las zonas relevantes. También prueba que UI y API estén de acuerdo sobre el estado de la misma cuenta con los mismos timestamps almacenados.
When should I bring in FixMyMess to fix trial and subscription logic?
Es momento de auditar cuando ves comportamientos contradictorios: la UI dice “trial active” pero la API bloquea, distintos endpoints calculan la expiración de forma diferente o las pruebas cambian por una hora alrededor de DST. FixMyMess puede hacer una auditoría de código gratuita para localizar checks dispersos, unidades de tiempo mezcladas y fugas de zona horaria, luego consolidarlo todo en un único punto de decisión probado para que las pruebas terminen exactamente cuando quieres.