Caso de estudio
Turnos
Aplicación web multi-rol donde los clientes reservan citas con profesionales.
- Rol
- Proyecto individual.
- Estado
- Terminado
Stack
- Python 3.12
- FastAPI
- SQLAlchemy 2.0
- Alembic
- Pydantic v2
- PostgreSQL 16
- PyJWT
- React
- TypeScript
- Vite
- Tailwind CSS
- TanStack Query
- Docker Compose
Problema
Coordinar citas entre clientes y profesionales sin cruzar horarios: reglas de disponibilidad, confirmación, recordatorios y permisos distintos para cada tipo de usuario.
Solución
- Reserva de citas a partir de la disponibilidad semanal del profesional y sus bloqueos por fechas.
- Reprogramación de reservas y exportación a calendario en formato .ics.
- Flujo de estados de la reserva (pendiente, confirmada, completada y cancelada) con correos transaccionales y recordatorios.
- Panel de administración con métricas y reportes descargables en CSV, Excel y PDF.
- Tema claro y oscuro, con diseño responsive.
Roles y flujos
- Cliente
- Se registra y verifica su correo, elige un horario libre y reserva; puede ver sus reservas, reprogramarlas, añadirlas a su calendario y cancelarlas con antelación.
- Profesional
- Gestiona su disponibilidad semanal y sus bloqueos, ve su agenda por día o semana y confirma, cancela o completa las reservas que le corresponden.
- Admin
- Administra servicios y usuarios, asigna servicios a los profesionales, ve todas las reservas y sus métricas, y descarga reportes por rango de fechas.
Arquitectura
Navegador
El cliente, el profesional o el admin.
Web
Vite y React, servida como frontend estático.
API
FastAPI, con autenticación por JWT y cookie de refresco.
Base de datos
PostgreSQL 16, con SQLAlchemy y Alembic.
Correo
Servicio SMTP para los correos transaccionales.
Decisiones técnicas
- Anti doble reserva en la base de datos
- Un constraint de exclusión de PostgreSQL impide solapar reservas pendientes o confirmadas del mismo profesional, incluso con peticiones simultáneas; la violación se traduce a una respuesta de horario no disponible.
- Refresh tokens con rotación y detección de reutilización
- El token de refresco vive en una cookie httpOnly y se rota en cada uso; si se reutiliza uno revocado, se revoca toda su familia. El token de acceso solo existe en memoria del frontend.
- Generación de horarios como función pura
- El cálculo de los espacios libres parte de reglas, bloqueos y reservas sin tocar la base de datos, lo que lo hace fácil de probar.
- Correos fuera de la transacción
- Las notificaciones se envían después de confirmar la reserva: un fallo de correo nunca revierte una reserva.
- Autorización en dos capas
- El rol se exige en los routers y la propiedad del recurso se comprueba en los servicios.
Retos y aprendizajes
- Evitar dobles reservas
- Se resolvió con un constraint de exclusión en PostgreSQL, de modo que la propia base de datos garantiza que no haya solapes aunque lleguen dos peticiones a la vez.
- Reprogramar sin romper esa regla
- El horario actual se libera dentro de la misma transacción para permitir corrimientos que se solapan con la propia reserva; si el nuevo horario no es válido, no se guarda nada.
- Auditoría de seguridad
- Se corrigieron hallazgos como el acaparamiento de horarios con reservas pendientes ilimitadas y la posibilidad de reservar sin verificar el correo; cada corrección quedó cubierta por pruebas de regresión.
Siguiente paso
- Pasarela de pagos y depósitos al reservar.
- Recordatorios por SMS o WhatsApp.
- Sincronización bidireccional con Google Calendar.
- Internacionalización y zona horaria por usuario.
Capturas



