El límite de horas que le faltaba a Calendly
Reservar una sesión con Calendly es fácil. Sin embargo, Calendly todavía está muy limitado cuando necesitas asignar X horas a un alumno al que le has vendido 10 sesiones de 1h individualizadas o un cliente al que le has vendido un servicio de consultoría por 10h al mes y en ambos casos tienes que controlar las horas que reserva sin tener que estar apuntando manualmente cada sesión.
En el caso de los alumnos, también está la complicación de gestionar que se comparta el enlace de sesiones con alumnos/personas a las que no les corresponde, ya sea con un enlace genérico o con un enlace de uso único.
Te cuento por aquí, cómo diseñé un plugin de WordPress para gestionar bonos de horas de sesiones 1:1, concretamente para un cliente con alumnos/as que compran una formación.
Las funcionalidades que contiene el plugin son las siguientes:
- Acceso por membresía
- Tutor/a asignado/a
- Horas disponibles
- Integración con Calendly
- Logs y diagnósticos, por si hay algún error
- Cancelación automática de reservas no válidas.
Los requisitos del cliente
Sección titulada «Los requisitos del cliente»Lo que el cliente pidió fue crear una solución para su plataforma online de formación en WordPress en la que sus alumnos tendrían acceso a X horas de sesiones individualizadas con sus tutores/as correspondientes.
Imprescindible: Ningún alumno/a podía pasar de las horas establecidas y, sobre todo, evitar añadir más herramientas al stack que tenía. Es decir, utilizar lo que ya tenía.
Y con esto y un bizcocho, “be water my friend”.
¡Con esta mínima información, me puse manos a la obra!
Por si vas con prisa, te lo resumo aquí
Sección titulada «Por si vas con prisa, te lo resumo aquí»El plugin hace que WordPress sea el que gestione el tiempo y el registro de cada una de las sesiones y usa Calendly solo como agenda.
La primera versión (el MVP) incluye:
- Bonos manuales de X horas por alumno/a. En este caso concreto eran 5 horas, pero el plugin lo guarda en minutos para poder trabajar con sesiones de 30, 45 o 60 minutos.
- Validación de acceso con WooCommerce Memberships.
- Tutor/a asignado/a por alumno/a.
- Calendly embebido con UTMs firmadas.
- Webhook de Calendly para recibir reservas y cancelaciones.
- Control de horas disponibles antes de mostrar el calendario y al recibir el webhook.
- Cancelación automática de reservas sin identificación o que superan las horas disponibles.
- Logs, diagnóstico y pantalla de notificaciones.
Los problemas principales que me encontré
Sección titulada «Los problemas principales que me encontré»Lo que el cliente necesitaba parecía muy sencillo (ja-ja-ja): un alumno/a entra en su área privada, reserva una sesión con su tutor/a y tiene un máximo de horas disponibles para utilizar cuando lo necesite. En este caso eran 5 horas.
La chicha estaba en todas las condiciones a tener en cuenta, de las cuales unas pocas no estaban ni pensadas:
- Solo algunos alumnos/as tienen derecho a sesiones 1:1.
- Cada persona tiene un bono limitado de horas.
- Cada alumno/a tiene un tutor/a asignado/a, lo que significaba que cada tutor/a tenía su propio Calendly.
- Las horas disponibles debían actualizarse en el momento de la reserva, la cancelación o la no asistencia.
- ¿Qué pasaba si se compartía el calendario de un tutor/a?
- ¿Qué pasaba si cancelaban a última hora? ¿O si directamente no asistían a la sesión?
Incluso había otro punto más delicado, diría que el que más me preocupaba: si alguien usaba un enlace público de Calendly fuera de la plataforma, la reserva no debería contar como válida ni agendarse. Es decir, el riesgo era que las personas más pillas compartieran ese enlace con sus compys más afines y se aprovecharan de ese agujero negro.
WordPress tenía que controlar el cotarro
Sección titulada «WordPress tenía que controlar el cotarro»La plataforma de formación del cliente ya vivía en WordPress, así que el plugin tenía sentido… Un script que hiciera su magia por detrás, siendo el responsable principal de los permisos para agendar o no, mientras que Calendly quedaba solo como herramienta de agenda.
Esta diferenciación es la que marcaba el camino, ya que Calendly es el que sabe que alguien ha reservado un hueco y WordPress sabe si esa persona podía hacerlo.
Para el plugin, la información que WordPress guarda es:
- El bono del alumno/a.
- El tutor/a asignado/a.
- Los minutos totales que le corresponden.
- Los minutos utilizados.
- Las sesiones registradas.
- Los logs e incidencias.
- La configuración de Calendly.
- El estado de la membresía de WooCommerce Memberships.
El alumno/a solo ve lo que puede utilizar
Sección titulada «El alumno/a solo ve lo que puede utilizar»El panel del alumno/a muestra lo mínimo:
- Horas totales.
- Tiempo utilizado.
- Tiempo disponible.
- Tutor/a asignado/a.
- Calendario de Calendly de su tutor/a.
- Historial de sesiones reservadas, realizadas, canceladas, no show o inválidas.
Condiciones
- Si ya no le quedan minutos suficientes para reservar, el calendario no se muestra.
- La sesión mínima es de 30 minutos. Si quedan menos de 30 minutos, WordPress muestra un aviso en lugar de Calendly. No espera a que Calendly acepte una reserva que luego habría que corregir a mano.
Cómo se identifica una reserva válida
Sección titulada «Cómo se identifica una reserva válida»Cuando WordPress muestra el Calendly embebido dentro de la plataforma, se añaden automáticamente parámetros UTM al enlace.
Estas UTMs no son UTMs de marketing, sino que uso estos parámetros para el contexto técnico. Calendly permite pre-rellenar algunos datos y respuestas a preguntas, pero no es lo mismo que tener campos ocultos arbitrarios para guardar información interna. Para pasar datos técnicos que luego lleguen en el webhook, la vía documentada son las UTMs. Por eso son las que ayudan al plugin a validar una reserva.
utm_source=nombre_de_las_sesionesutm_medium=balance_guardutm_campaign=sesiones_1_1utm_content=<id_del_bono>utm_term=<token_firmado>La clave está en utm_content y utm_term.
utm_content identifica el bono que se le asigna al alumno y utm_term lleva un token firmado que WordPress puede verificar.
Así, aunque el alumno/a cambie el email dentro de Calendly, WordPress puede saber qué bono estaba usando realmente.
Qué pasa si alguien reserva fuera de la plataforma
Sección titulada «Qué pasa si alguien reserva fuera de la plataforma»Calendly trabaja muy bien con enlaces públicos y enlaces únicos. El problema es que cualquiera de esos enlaces se puede copiar, reenviar o guardar. Si es público, se puede utilizar todas las veces que se quiera. Y si es único, con compartirlo y usarlo una vez, ya puede causar una reserva de alguien desconocido.
Al principio parecía que la solución ideal era generar enlaces únicos desde WordPress usando la API de Calendly, pero igualmente, el enlace único se podía compartir, aunque solo se pudiera usar una sola vez. Dentro de lo que cabe, era un “daño menor”.
Como no me quería conformar con el daño menor, implementé LA REGLA: si una reserva de Calendly llega sin UTMs válidas, WordPress la considera no identificada.
En ese caso el plugin:
- RegistraRegistra la incidencia.
- No consumeNo consume horas de nadie, porque no está identificado.
- CancelaWordPress manda la orden a Calendly de cancelar automáticamente el evento.
- LogDeja un log visible en Diagnóstico.
Cuando el plugin cancela el evento, Calendly envía su email normal de cancelación a la persona que había reservado. Así no se queda una reserva aparentemente activa por un lado y cancelada por otro.
Y esta es la parte potente: ni un enlace público ni uno único se puede compartir libremente. Incluso si un tutor/a comparte el calendario dedicado a las sesiones 1:1, WordPress recibe el aviso, ve que no viene con los parámetros correctos y lo cancela.
Límite real del sistema
Sección titulada «Límite real del sistema»Es decir, no se reserva gratis ni queda fuera del sistema: consume o bloquea sus propias horas, y si supera el límite, se invalida y se cancela.
Para este MVP me parecía un intercambio razonable. No buscaba una barrera perfecta, sino que cualquier reserva quedara identificada, limitada por las horas disponibles y visible en logs, además de que iba con el tiempo limitadísimo para hacer una primera versión.
El súper webhook
Sección titulada «El súper webhook»Configuré Calendly para que enviara eventos a WordPress cuando alguien reserva o cancela:
invitee.createdinvitee.canceledEn producción terminé usando admin-ajax.php para recibir el webhook porque la REST API pública estaba bloqueada por la configuración de seguridad de la web. No era la opción más bonita sobre el papel, pero sí la que funcionaba de forma estable en esa instalación.
El endpoint es público, así que no podía quedar abierto sin más. Por eso valida un secreto compartido antes de procesar nada y, después, valida el token firmado de las UTMs para saber si la reserva pertenece a un bono real.
El plugin recibe esos eventos y los clasifica en tres grandes casos:
- Reserva válida: viene desde la plataforma, trae las UTMs correctas y hay horas disponibles.
- Reserva no identificada: llega sin UTMs o con un token que WordPress no puede validar.
- Reserva fuera de límite: viene identificada, pero supera las horas disponibles del bono.
Con esa clasificación, el plugin decide si registra la sesión, si no consume horas o si manda la orden a Calendly para cancelar automáticamente el evento.
Una recomendación por tu salud mental: Implementar una pantalla de diagnóstico
Sección titulada «Una recomendación por tu salud mental: Implementar una pantalla de diagnóstico»Una de las mejores decisiones fue añadir una pestaña de Diagnóstico, porque prácticamente evitaba trabajar a ciegas, ya que cuando integras WordPress, Calendly y WooCommerce, necesitas una forma rápida de saber dónde se ha roto algo. Si no, acabas depurando con capturas, intuición y café esperando a que te lo tomes.
Ahí se puede ver:
- Versión activa del plugin.
- Si WooCommerce Memberships está detectado.
- Si Calendly está configurado.
- Si el webhook está preparado.
- Si el modo test está activo.
- Próxima ejecución del cron.
- Últimos logs.
- Incidencias recientes.
- Webhook recomendado.
Es la forma de saber por qué una reserva se ha aceptado, cancelado o marcado como inválida sin tener que reconstruirlo todo a mano.
Las horas disponibles se controlan dos veces
Sección titulada «Las horas disponibles se controlan dos veces»El plugin evita mostrar el calendario de Calendly del tutor/a correspondiente si el alumno/a no tiene suficientes horas disponibles. Sin embargo, la gente llega a ser muy pícara, por lo que alguien podría tener abierto el calendario en otra pestaña, reservar dos veces muy rápido o usar un enlace antiguo, por eso las horas disponibles se vuelven a comprobar cuando llega el webhook.
En ese punto el plugin recalcula los minutos utilizados y bloquea temporalmente el bono para evitar carreras entre reservas simultáneas.
Si la reserva supera las horas disponibles:
- Se marca como
invalida. - No consume horas.
- Se muestra en rojo.
- Se intenta cancelar automáticamente en Calendly.
- Se guarda el motivo.
El motivo puede ser:
La sesión se ha cancelado automáticamente por superar el límite de tiempo del bono.Los estados
Sección titulada «Los estados»Acabé implementando estos estados:
reservadarealizadacanceladano_asistioinvalidaCada uno afecta a las horas disponibles de forma distinta.
reservada, realizada y no_asistio consumen o bloquean tiempo. cancelada devuelve el tiempo. invalida no consume horas y sirve para señalar que algo no debía aceptarse.
Cuando llega una reserva válida desde Calendly, queda como reservada. Después, un cron revisa las sesiones pasadas y las marca como realizada si no han sido canceladas. El estado no_asistio queda como acción manual, porque ahí no basta con que la sesión haya pasado: alguien tiene que confirmar que la persona no se presentó. En este MVP lo confirma administración desde WordPress, normalmente después de que el tutor/a lo comunique.
El acceso rápido
Sección titulada «El acceso rápido»Además del panel completo, añadí un shortcode de widget para que el alumno/a pudiese acceder directamente desde su dashboard.
En este caso, si el usuario no tiene bono, no muestra nada de nada. Si tiene bono, muestra una tarjeta con acceso directo a la página de sesiones 1:1. Y si tiene bono pero la membresía todavía no está activa, muestra un mensaje de que el bono se activará en breve.
Esto evita enseñar funcionalidades a quien no tiene que verlas y reduce preguntas innecesarias.
Estructura
Sección titulada «Estructura»El archivo principal estaba creciendo demasiado, así que lo dividí por responsabilidad:
Directorioincludes/
- class-core.php
- class-admin-actions.php
- class-calendly.php
- class-data.php
- class-repositories.php
- class-student.php
Directorioadmin-pages/
- class-admin-settings-page.php
- class-admin-students-page.php
- class-admin-tutors-page.php
- class-admin-sessions-page.php
- class-admin-notifications-page.php
- class-admin-diagnostics-page.php
También centralicé los estados de sesión para evitar strings repetidos por todo el código.
Lo que me llevo
Sección titulada «Lo que me llevo»Cuanto más sencilla es la solución, más fácil es gestionar las condiciones. En este caso, darle la batuta a WordPress y definir bien las reglas simplifica todo lo demás: horas disponibles, permisos, logs, incidencias, cancelaciones y soporte.