SD Radovljica
Un portal de socios a medida para un club de tiro esloveno — inscripciones a entrenamientos, gestión de competiciones, clasificaciones de temporada y feeds iCal, construido como un plugin de WordPress a medida con trazas de auditoría de nivel fintech.
Resumen
La mayoría de los clubes deportivos gestionan a sus socios en hojas de cálculo. SD Radovljica — un club de tiro de Radovljica, Eslovenia — quería algo mejor: un portal de socios en condiciones donde cada socio registrado pueda navegar por el calendario de entrenamientos, inscribirse a sesiones, entrar en competiciones y ver su clasificación de temporada, todo dentro de un sistema seguro y controlado por roles.
Lo construí como un plugin de WordPress a medida — no como un hack de tema ni como un montón de shortcodes, sino como un paquete PHP PSR-4 de primera clase con arquitectura por capas (Models → Repositories → Services → API / Admin / Frontend), una suite completa de PHPUnit con Brain Monkey y wp-phpunit, análisis estático vía PHPStan y WordPress Coding Standards aplicados en CI.
El sitio público es un tema hijo de GeneratePress portado desde mockups HTML aprobados. El trabajo diferenciador es el plugin: un sistema autocontenido de gestión de socios que costaría cinco cifras como SaaS de catálogo pero que corre dentro de un hosting cPanel de 10 €/mes — porque cada pieza fue construida exactamente a lo que el club necesita.
Arquitectura
Leyendo el diagrama: Cinco superficies — el sitio público GeneratePress, el portal orientado al socio (shortcodes), el panel de administración de WordPress (7 páginas de gestión), los feeds iCal y el pipeline de emails de WP-Cron — todas hablan con un único núcleo de plugin. Diez tablas de base de datos a medida manejan el modelo de datos completo: los socios son usuarios nativos de WordPress extendidos con cuatro roles a medida y 14 capacidades. Cada escritura que importa — inscripción, entrada de resultado, asignación de turno, cancelación — se registra en la tabla de auditoría con dirección IP y user-agent.
El portal de socios en detalle
El portal tiene cuatro niveles de rol, cada uno con capacidades explícitas establecidas en el momento de activación. Un Socio SDR (SDR Član) puede inscribirse en sesiones de entrenamiento y competiciones, ver resultados y ver sus propias inscripciones. Un Admin de Competición SDR añade la capacidad de gestionar sesiones, gestionar competiciones, introducir resultados y asignar turnos. Un Miembro de Junta SDR (SDR Član IO) obtiene acceso de lectura a los documentos de la junta — actas de reuniones, registros de asambleas, reglamentos — que son invisibles para los socios corrientes. Los administradores tienen las 14 capacidades.
Calendario de entrenamientos. Las sesiones se almacenan en una tabla a medida con disciplina, tipo de evento (entrenamiento, competición, sesión de dynamics, visita, otro), ubicación, capacidad y recurrencia opcional (semanal, quincenal, mensual). Los socios se inscriben a través de un frontend renderizado por shortcode; la aplicación de capacidad y la detección de conflictos de horario ocurren en el lado del servidor. Las marcas de tiempo de inscripción se almacenan con precisión de microsegundo (datetime(6)) — cuando una sesión se llena en exactamente el mismo segundo, gana el microsegundo más temprano, dando un orden de cola determinista y justo sin locks a nivel de aplicación.
Gestión de competiciones. Las competiciones tienen un ciclo de vida explícito: borrador → inscripciones abiertas → inscripciones cerradas → en curso → completada → cancelada. Los socios envían hasta tres preferencias de franja horaria; los admins asignan un turno confirmado y número de calle a través de una interfaz admin AJAX. Las transiciones de estado — pending, confirmed, DNS, DNF — siguen cada etapa del recorrido competitivo del socio.
Clasificaciones de temporada. Cada resultado de competición lleva una puntuación, categoría (junior / senior / veterano) y un flag is_ranked. Una regla configurable best-of-N (por defecto: las 5 mejores de 10 por temporada) impulsa el Ranking_Calculator. En lugar de un cron job que recomputa todo en un horario, las clasificaciones se reconstruyen de forma síncrona en el momento en que se guarda un resultado — la action sdr_result_saved se dispara dentro del repositorio, activa la calculadora limitada a ese par (discipline_id, season_year), y la tabla de líderes está actualizada antes de que regrese la respuesta HTTP. El desempate dense-rank (1,1,3 en lugar de 1,2,3) coincide con cómo se presentan convencionalmente los resultados de tiro.
Integración iCal. El plugin genera feeds .ics conformes a RFC 5545 — un feed público por disciplina o tipo de evento y un feed personal por socio (sólo sus propias inscripciones). Los feeds personales están protegidos por un token de 32 caracteres almacenado en user meta; el patrón de URL es /sdr-calendar/personal/<user_id>/<token>/. Los socios se suscriben una vez desde Google Calendar o Apple Calendar y se mantienen sincronizados automáticamente.
Automatización de email. Tres rutas automatizadas: confirmación de inscripción al registrarse (entrenamiento y competición, conmutables por separado), notificación de publicación de resultados y un recordatorio diario por WP-Cron que se dispara a las 08:00 y envía email a todos los inscritos a una sesión en las próximas 23–25 horas. El recordatorio usa una plantilla PHP en templates/email/reminder-training.php; cada envío — éxito o fallo — escribe una fila en el registro de auditoría para que la junta pueda ver la entrega de un vistazo.
Biblioteca de documentos. Los documentos de la junta (actas de reuniones, registros de asambleas, reglamentos, formularios) se almacenan con tres niveles de acceso: público, socios y junta. La taxonomía de tipos de documento mapea limpiamente a cómo los clubes deportivos eslovenos están legalmente obligados a mantener sus registros.
GDPR. Eslovenia está en la UE. El plugin se registra con las herramientas nativas de privacidad de WordPress — un exportador nombrado y un borrador nombrado bajo Herramientas → Exportar / Borrar datos personales. El borrador elimina las inscripciones pero anonimiza las filas de resultado: user_id se establece a NULL (nullable a nivel de esquema desde la versión 1.1.0 de la BD), las puntuaciones y marcas de tiempo se preservan, de modo que la solicitud de borrado de un socio no abre agujeros en las tablas de líderes históricas del club.
Línea base de calidad. El código corre PHPStan en nivel 5 con el plugin szepeviktor/phpstan-wordpress, PHPCS contra WordPress Coding Standards y una suite híbrida de PHPUnit — Brain Monkey para tests unitarios puros de Services y Models (sin bootstrap de WordPress requerido), wp-phpunit para tests de integración de repositorios, controladores REST y shortcodes. Cada push de CI ejecuta las tres herramientas.
Por qué importa
Este es el tipo de proyecto que ilustra lo que “T4 Care” significa realmente en la práctica. El club tiene quizá sesenta socios activos. Nadie notaría si las marcas de tiempo de inscripción fueran de precisión de segundo en lugar de microsegundo. Nadie notaría si el borrado GDPR simplemente eliminara las filas de resultado. Nadie notaría si no hubiera registro de auditoría.
Lo construí con esos detalles de todos modos — porque los sesenta socios son personas reales, porque la aplicación de GDPR en Eslovenia es real y porque el secretario del club merece una infraestructura que no le ponga en evidencia dentro de dos años. La escala es una fracción de una plataforma fintech. El cuidado no lo es.
El backend de un club pequeño merece el mismo rigor que una plataforma fintech. Trazas de auditoría, borrado GDPR, inscripciones a prueba de carreras — la escala es diferente, pero los socios del club son igual de reales.
Seis cosas entregadas,
tres difíciles resueltas.
Contribuciones clave
- Diseñé y construí el plugin de WordPress completo a medida desde cero — arquitectura PSR-4 OOP a través de las capas Models, Repositories, Services, API, Admin y Frontend.
- Implementé un sistema de roles y capacidades de cuatro niveles (socio, admin de competición, miembro de junta, administrador) con 14 permisos granulares.
- Construí la gestión de sesiones de entrenamiento con eventos recurrentes, límites de capacidad, detección de conflictos y marcas de tiempo de inscripción con precisión de microsegundo para resolver de forma justa las inscripciones simultáneas.
- Construí la gestión de competiciones con asignación de turnos multi-día, asignación de calle y un sistema de tres preferencias para que los socios influyan en su propio horario.
- Implementé un motor de clasificación de temporada: algoritmo dense-rank por disciplina, reglas configurables de puntuación best-of-N, recálculo dirigido por eventos en cada guardado de resultado.
- Construí la generación de feeds iCal (público + feed personal por socio autenticado por token) para que los socios sincronicen el calendario del club directamente con Google Calendar o Apple Calendar.
- Cableé un pipeline diario de recordatorios con WP-Cron que envía alertas por correo a las 08:00 para las sesiones del día siguiente — con entradas en el registro de auditoría por cada envío y aislamiento de fallos.
- Integré el exportador y borrador nativos de GDPR de WordPress — las inscripciones se eliminan a petición, los resultados de competición se anonimizan en lugar de borrarse para mantener intactas las clasificaciones históricas.
- Monté una suite completa de PHPUnit (Brain Monkey para unit, wp-phpunit para integración) con análisis estático PHPStan y PHPCS (WordPress Coding Standards) en CI.
Desafíos resueltos
- Carreras de inscripciones simultáneas — resueltas con marcas de tiempo
datetime(6)con precisión de microsegundo y una restricción única sobre(session_id, user_id), dando un orden de cola determinista sin locks a nivel de aplicación. - Tensión entre GDPR e integridad histórica — los resultados de competición debían sobrevivir a las solicitudes de borrado sin corromper las clasificaciones de temporada; resuelto anonimizando la columna
user_ida NULL en lugar de borrar la fila del resultado. - Recálculo de clasificaciones dirigido por eventos — descartando el enfoque basado en cron a favor de un action hook síncrono
sdr_result_savedque reconstruye la caché de clasificación por(discipline, season)inmediatamente al insertar o actualizar un resultado.
Qué hay bajo el capó.
¿Listo para arreglar, construir
o escalar?
30 minutos, conmigo personalmente. Leo tu sistema como un archivo de logs y te digo qué haría primero. Sin presentaciones, sin embudo de ventas.
— Davor Majc, fundador, Numen

