Plataforma web para administrar socios, cuotas, pagos y reportes del Burzaco Sportclub — construida con arquitectura hexagonal, desplegada en la nube.
El Burzaco Sportclub administraba socios, cuotas y deudas con planillas dispersas. Sin un sistema central, era difícil saber quién pagó, quién debe, y generar reportes confiables para la toma de decisiones.
Información de socios repartida en planillas sin una fuente única de verdad.
Difícil rastrear pagos, deudas y períodos por socio.
Imposible ver recaudación, morosidad o crecimiento de un vistazo.
Un backend REST que concentra toda la lógica de negocio y una base única, con un panel de administración limpio para operar el día a día del club.
Flask + arquitectura hexagonal. Concentra reglas de negocio, autenticación y persistencia. Corre en el puerto 5001.
Server-side rendering con Jinja2. Login, gestión de socios, registro de pagos y reportes. Puerto 5000.
El dominio no depende de nada externo. La lógica de negocio vive aislada; bases de datos y frameworks son detalles intercambiables conectados por interfaces.
El dominio (centro) no sabe si los datos vienen de SQLite o Supabase. Eso se decide afuera y se inyecta — el corazón del patrón.
Alta, baja, edición y búsqueda con filtros (actividad, categoría, estado, cuota). Paginación incluida.
Login por DNI y contraseña. El backend firma un token JWT (válido 8 h); el frontend lo guarda y lo manda en cada llamada protegida. Contraseñas hasheadas con bcrypt.
Registrar cuotas por socio (monto, concepto, período), ver historial completo y resumen: total pagado, montos y períodos cubiertos.
Tarjetas de total de socios, morosos y recaudación; distribución por actividad y cobranzas por período.
Botón "Sincronizar" que actualiza las estadísticas con datos reales del backend en un click.
El operador entra DNI + contraseña en el panel.
El backend busca el usuario y compara la contraseña con bcrypt contra el
hash guardado.
Si es válido, firma un JWT (HS256) con DNI, nombre y rol. Expira en 8 horas.
El frontend guarda el token y lo manda como Authorization: Bearer en cada
pedido protegido.
# POST /api/auth/login { "dni": "00000000", "password": "••••••••" } # 200 OK { "token": "<JWT>" }
No usamos clases por usar: cada pilar resuelve un problema concreto de mantenibilidad.
La entidad Socio guarda su estado y sus reglas juntas:
pagar_cuota() valida antes de cambiar el estado.
Las excepciones heredan de Exception; los repositorios concretos heredan del
puerto abstracto.
Puertos como SocioRepository (ABC) definen el contrato sin implementarlo.
Un mismo servicio recibe SQLite o Supabase indistintamente. No sabe ni le importa cuál es.
# application/ports.py — la abstracción class SocioRepository(ABC): @abstractmethod def get_by_dni(self, dni): ... # app.py — polimorfismo + inyección if supabase_url and supabase_key: repo = SupabaseSocioRepository(...) else: repo = SQLiteSocioRepository(...) service = SocioService(repo) # DIP (la D de SOLID)
Separamos el QUÉ del CÓMO: el servicio trabaja contra la abstracción, no contra la base de datos concreta.
Cada push a main dispara un pipeline que prueba, construye la imagen y la despliega —
sin pasos manuales.
Un cambio entra al repositorio.
GitHub Actions corre los tests y verifica que la app arranca antes de seguir.
Se construye la imagen Docker del backend y del frontend.
Las imágenes se publican y se despliegan en GCP Cloud Run. Escala solo según la demanda.