This commit is contained in:
Laucha1312
2026-06-04 15:15:23 -03:00
parent 0841794c50
commit 90c5f85512
167 changed files with 15870 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,244 @@
# OnAPB Genius Agent — Spec de Diseño
**Fecha:** 2026-04-09
**Rama:** `feature/genius-agent`
**Stack:** Laravel 12, Prism PHP (prism-php/prism), Google Gemini 1.5 Flash, MySQL, Alpine.js
---
## 1. Objetivo
Implementar un agente de IA conversacional ("OnAPB Genius") integrado en onapb.com con dos modos de operación según el rol del usuario:
- **Público** (no logueado, aficionado, jugador): asistente de navegación y consultas usando el `MANUAL_USUARIO.md` como contexto RAG simplificado. Sin tools. Sin persistencia entre sesiones.
- **Admin** (SuperAdmin rol=1 / GeneralAdmin rol=2): automatización de tareas mediante function calling (Tools). Historial persistente en MySQL, auto-purgado a los 30 días.
---
## 2. Decisiones de Arquitectura
| Decisión | Elección | Razón |
|---|---|---|
| SDK de AI | `prism-php/prism` | Soporte probado de Gemini 1.5 Flash + tools. Más estable que `laravel/ai` (nuevo). |
| Modelo | `gemini-1.5-flash` | Velocidad (25s), costo bajo, function calling. |
| Streaming | No — JSON completo | Hosting compartido Hostinger con límites PHP desconocidos. Evita problemas de output buffer. |
| Memoria admin | MySQL (`agent_threads`) | Sin Redis. JSON column para mensajes. Purge automático 30 días. |
| Memoria público | Session PHP | Stateless entre sesiones. Simple, sin overhead de BD. |
| Tools | Solo si admin_logged_in | Validado en `GeniusAgentService`, no en Gemini. |
---
## 3. Estructura de Archivos
```
app/
├── AI/
│ ├── Tools/
│ │ ├── CrearPartidoTool.php
│ │ ├── CargarPuntajeTool.php
│ │ ├── RedactarNoticiaTool.php
│ │ ├── ListarEquiposTool.php
│ │ └── ListarEventosTool.php
│ └── Prompts/
│ ├── SystemPromptAdmin.php
│ └── SystemPromptPublic.php
├── Services/
│ └── GeniusAgentService.php
├── Http/Controllers/
│ └── GeniusAgentController.php
├── Models/
│ └── AgentThread.php
└── Console/Commands/
└── PurgeAgentThreads.php
database/migrations/
└── xxxx_create_agent_threads_table.php
resources/views/components/
└── genius-chat.blade.php
routes/web.php
└── POST /agent/chat (throttle: 20/min por IP)
```
---
## 4. Schema de Base de Datos
```sql
CREATE TABLE agent_threads (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
thread_id VARCHAR(36) NOT NULL UNIQUE, -- UUID generado en frontend
admin_id INT NOT NULL, -- session('admin_id')
messages JSON NOT NULL, -- array [{role, content}]
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL -- created_at + 30 días
);
```
---
## 5. Tools Disponibles (Admin)
Cada tool implementa la interfaz Prism `Tool`. El agent loop permite que Gemini llame la misma tool N veces para operaciones batch (ej. cargar puntajes de todos los partidos de una jornada).
| Tool | Parámetros | Acción en BD |
|---|---|---|
| `CrearPartidoTool` | `id_equipo_local`, `id_equipo_visitante`, `fecha_evento`, `hora_inicio`, `hora_fin`, `sede`, `id_torneo` | `Evento::create(...)` |
| `CargarPuntajeTool` | `id_evento`, `marcador_local`, `marcador_visitante` | `Evento::find()->update(...)` |
| `RedactarNoticiaTool` | `titulo`, `contenido`, `id_torneo?`, `categoria?` | `Noticia::create(...)` |
| `ListarEquiposTool` | `id_torneo?`, `id_club?`, `grupo?` | `Equipo::query()` con join a pivot `torneo_equipo` filtrando por `grupo` (solo lectura) |
| `ListarEventosTool` | `fecha_desde?`, `fecha_hasta?`, `id_torneo?` | `Evento::query()->get()` (solo lectura) |
**Nota sobre `grupo`:** No es un modelo separado. Es una columna en la tabla pivot `torneo_equipo` (relación `Torneo::equipos()->withPivot('grupo')`). `ListarEquiposTool` filtra con `wherePivot('grupo', $grupo)`.
**Agregar una nueva tool en el futuro:**
1. Crear `app/AI/Tools/NuevaTool.php`
2. Registrarla en `GeniusAgentService::getAdminTools()`
---
## 6. Flujo de Datos
### Usuario público
```
POST /agent/chat { message }
→ session()->get('agent_messages', [])
→ GeniusAgentService::chatPublic(message, history)
→ System prompt: navegación + MANUAL_USUARIO.md completo como contexto (el archivo es pequeño; si crece >50KB usar solo las primeras 200 líneas)
→ Prism → Gemini (sin tools)
→ Respuesta de texto
→ session()->put('agent_messages', [...])
→ return JSON { reply }
```
### Admin
```
POST /agent/chat { message, thread_id? }
→ AgentThread::findOrCreate(thread_id, admin_id)
→ GeniusAgentService::chatAdmin(message, thread)
→ System prompt: automatización de tareas OnAPB
→ Prism → Gemini con tools
→ [si Gemini llama tool] → Tool::handle() → resultado
→ [Gemini puede llamar N tools] → agent loop
→ Respuesta final de texto
→ thread->appendMessages([...])
→ thread->save()
→ return JSON { reply, thread_id }
```
---
## 7. Seguridad
- **Tools bloqueadas a no-admins:** `GeniusAgentService` valida `session('admin_logged_in')` antes de incluir tools.
- **Rate limiting:** `throttle:20,1` en la ruta `/agent/chat`.
- **Validación de input:** `message` requerido, string, máx. 1000 caracteres.
- **Aislamiento de threads:** cada thread valida `admin_id = session('admin_id')`. No hay acceso cruzado entre admins.
- **API key:** solo en `.env` (`GEMINI_API_KEY`). Nunca en código ni en BD.
---
## 8. Manejo de Errores
| Caso | Comportamiento |
|---|---|
| Gemini timeout / 5xx | `catch RequestException` → JSON `{ error: "El agente no responde, reintentá en un momento." }` |
| Tool falla (ej. evento no existe) | Tool retorna `{ error: "..." }` → Gemini lo incorpora en su respuesta |
| Thread expirado / no encontrado | Se crea un nuevo thread automáticamente |
| API key inválida / quota excedida | Log en Laravel + respuesta genérica (sin exponer detalles de la API) |
| Timeout de Hostinger | `set_time_limit(120)` al inicio del controller action |
---
## 9. Purge de Threads
Comando `php artisan agent:purge-threads` que elimina `agent_threads` donde `expires_at < NOW()`.
**Schedule:** Se registra en `routes/console.php` para ejecutarse diariamente. En Hostinger sin queue worker, se puede configurar como cron en hPanel:
```
# Reemplazar /home/TU_USUARIO/public_html con la ruta real de tu cuenta Hostinger
0 3 * * * cd /home/TU_USUARIO/public_html && php artisan agent:purge-threads >> /dev/null 2>&1
```
---
## 10. Frontend — Chat Bubble
Componente Blade `genius-chat.blade.php` incluido al final de `resources/views/layouts/app.blade.php`:
- Botón flotante (bottom-right) con ícono de chat
- Panel slide-up con historial de mensajes de la sesión actual
- Alpine.js para estado local (open/closed, messages, loading spinner)
- Tailwind para estilos (sin dependencias adicionales)
- Rol-aware: el frontend no diferencia, la diferencia la hace el backend
---
## 11. Comandos de Deploy
### Instalación inicial (local y producción)
```bash
# 1. Instalar Prism PHP
composer require prism-php/prism
# 2. Publicar config de Prism (opcional)
php artisan vendor:publish --provider="EchoLabs\Prism\PrismServiceProvider"
# 3. Agregar en .env
GEMINI_API_KEY=tu_api_key_aqui
# 4. Ejecutar migration de agent_threads
php artisan migrate
# 5. Registrar el comando de purge (se hace automáticamente con el código)
# Verificar que está en routes/console.php
# 6. Limpiar cachés después del deploy
php artisan config:clear
php artisan cache:clear
php artisan view:clear
php artisan route:clear
# 7. Re-cachear para producción
php artisan config:cache
php artisan route:cache
php artisan view:cache
```
### Comandos de mantenimiento
```bash
# Purge manual de threads expirados
php artisan agent:purge-threads
# Ver logs del agente
php artisan pail --filter="GeniusAgent"
# Limpiar todos los threads (emergencia)
php artisan tinker --execute="App\Models\AgentThread::truncate();"
```
### Deploy en Hostinger (via SSH o File Manager)
```bash
# Subir archivos nuevos y correr:
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
```
---
## 12. Configuración `.env` requerida
```env
GEMINI_API_KEY=tu_api_key_de_google_ai_studio
# Opcional: ajustar timeout HTTP de Prism
PRISM_HTTP_TIMEOUT=30
```
@@ -0,0 +1,376 @@
# Diseño: Sistema de Pagos con Banco Macro
**Fecha:** 2026-04-24
**Estado:** Aprobado por el usuario — listo para plan de implementación
**Proyecto:** OnAPB v2 (Laravel / Hostinger Business)
---
## Contexto
OnAPB v2 es el sistema de gestión de la Asociación Paranaense de Básquet. Se busca implementar un sistema de cobros digitales integrado con Banco Macro (Macro Click de Pago — Botón Integrado), centralizado en la cuenta de la APB.
- Framework: Laravel (PHP)
- Roles del sistema: super admin (role=1), admin de club (role=2), jugadores/aficionados (usuarios registrados), anónimos
- Existía una integración con MercadoPago que fue eliminada
- Las credenciales del Botón Integrado de Macro fueron solicitadas el 2026-04-23 y están pendientes de recibir
- Contacto Macro: Diego Dallanora — diegodallanora@macro.com.ar — +54 3794 15-0073
---
## Decisiones de diseño
| Decisión | Resolución |
|---|---|
| Pasarela de pago | Macro Click de Pago — **Botón Integrado** |
| Quién recauda | **La APB centraliza** todos los pagos (un solo merchant Macro) |
| Entrega tienda | **Retiro físico en sede** (sin envíos) |
| Sanciones | Super admin las carga manualmente sobre un jugador |
| Modelo de datos | **Tabla polimórfica de pagos** (Opción A) |
## Comisiones Macro (propuesta comercial vigente)
| Medio | Comisión | Acreditación |
|---|---|---|
| Tarjeta de crédito | 3.05% | 18 días hábiles |
| Tarjeta de crédito en cuotas | 3.05% | 18 días hábiles |
| Tarjeta de débito | 3.00% | 1 día hábil |
| DEBIN | 3.00% | 1 día hábil |
---
## Descomposición en sub-proyectos
Implementar en este orden (cada uno depende del anterior):
1. **Motor de pagos** — integración Macro base, modelo de datos, webhook
2. **Cobros institucionales** — inscripciones, multas/sanciones
3. **Tienda online** — catálogo, carrito, checkout
---
## Sección 1 — Modelo de datos
### Tablas nuevas
#### `concepto_pagos` — plantillas configurables por el super admin
| Campo | Tipo | Notas |
|---|---|---|
| `id` | PK | |
| `nombre` | string | ej: "Inscripción Anual Jugador 2026" |
| `descripcion` | text | |
| `monto` | decimal(10,2) | |
| `tipo` | enum | `inscripcion_jugador`, `inscripcion_equipo`, `multa`, `tienda` |
| `temporada` | string nullable | ej: "2026" — para conceptos anuales |
| `activo` | boolean | default true |
| timestamps | | |
#### `pagos` — registro de transacciones (polimórfico)
| Campo | Tipo | Notas |
|---|---|---|
| `id` | PK | |
| `concepto_pago_id` | FK → concepto_pagos | |
| `pagable_type` | string | App\Models\Jugador / Club / OrdenTienda / Sancion |
| `pagable_id` | unsignedBigInt | |
| `monto` | decimal(10,2) | snapshot al crear el pago |
| `estado` | enum | `pendiente`, `pagado`, `fallido`, `cancelado` |
| `macro_transaction_id` | string nullable | ID devuelto por Macro |
| `macro_payload` | json nullable | respuesta raw de Macro |
| `paid_at` | timestamp nullable | |
| `iniciado_por_type` | string nullable | quién inició (jugador, admin_user, null=anónimo) |
| `iniciado_por_id` | unsignedBigInt nullable | |
| timestamps | | |
#### `sanciones` — registros disciplinarios
| Campo | Tipo | Notas |
|---|---|---|
| `id` | PK | |
| `jugador_id` | FK → jugadores | |
| `id_club` | FK → clubes | club al momento de la sanción |
| `motivo` | string | |
| `descripcion` | text nullable | |
| `fecha_sancion` | date | |
| `admin_id` | FK → admin_users | quién la cargó |
| timestamps | | |
El pago de la sanción vive en `pagos` con `pagable_type = App\Models\Sancion`.
#### `productos` — catálogo de la tienda
| Campo | Tipo | |
|---|---|---|
| `id` | PK | |
| `nombre` | string | |
| `descripcion` | text nullable | |
| `precio` | decimal(10,2) | |
| `stock` | unsignedInt | |
| `imagen` | string nullable | |
| `activo` | boolean | default true |
| timestamps | | |
#### `ordenes_tienda` — órdenes de compra
| Campo | Tipo | Notas |
|---|---|---|
| `id` | PK | |
| `user_id` | FK nullable → users | null si comprador anónimo |
| `nombre_comprador` | string | capturado en checkout |
| `email_comprador` | string | para enviar comprobante |
| `estado` | enum | `pendiente_pago`, `pagado`, `listo_retiro`, `retirado`, `cancelado` |
| timestamps | | |
#### `orden_items` — líneas de cada orden
| Campo | Tipo | Notas |
|---|---|---|
| `id` | PK | |
| `orden_id` | FK → ordenes_tienda | |
| `producto_id` | FK → productos | |
| `cantidad` | unsignedInt | |
| `precio_unitario` | decimal(10,2) | snapshot al momento de compra |
### Relaciones Eloquent
```
ConceptoPago hasMany Pago
Pago morphTo pagable (Jugador | Club | OrdenTienda | Sancion)
Sancion belongsTo Jugador, Club, AdminUser
OrdenTienda hasMany OrdenItem
OrdenItem belongsTo Producto
```
---
## Sección 2 — Flujo de pago con Macro (Botón Integrado)
### Flujo estándar (aplica a todos los conceptos de pago)
```
Usuario decide pagar un concepto
Sistema crea registro en `pagos` (estado = pendiente)
GET /checkout/{pago}
Página con botón Macro embebido (monto + referencia interna precargados)
Usuario interactúa con el formulario de Macro
(tarjeta crédito / débito / DEBIN)
┌──────────────────┬─────────────────┐
↓ ↓ ↓
Pago exitoso Pago fallido Abandona
↓ ↓ ↓
Macro → /pagos/ Macro → /pagos/ Pago queda pendiente
exitoso fallido (expira por cron)
POST /api/macro/webhook ← fuente de verdad
Valida firma de Macro
Actualiza pago: estado = pagado, paid_at = now(), macro_payload = {...}
Dispara evento post-pago según pagable_type
Envía email de comprobante al pagador
```
**Regla crítica:** El webhook es la fuente de verdad, no la redirección. Las páginas de éxito/fallo son solo UX.
### Eventos post-pago por tipo
| `pagable_type` | Acción tras confirmar pago |
|---|---|
| `Jugador` (inscripción) | Marcar jugador como inscripto en la temporada |
| `Club` (inscripción equipo) | Marcar equipo/club como habilitado para participar |
| `Sancion` | Marcar sanción como saldada |
| `OrdenTienda` | Estado → `pagado`; notificar al super admin |
### Rutas nuevas
```
GET /checkout/{pago} → página con botón Macro embebido
GET /pagos/exitoso → pantalla de éxito (UX)
GET /pagos/fallido → pantalla de fallo (UX)
POST /api/macro/webhook → receptor del webhook (excluido de CSRF)
GET /tienda → catálogo público
GET /tienda/carrito → carrito de compras
POST /tienda/checkout → genera orden + pago, redirige a /checkout/{pago}
```
### Seguridad del webhook
- Verificar firma de Macro antes de procesar (algoritmo a confirmar con Macro)
- El monto siempre se toma de `pagos.monto`, nunca del payload entrante
- Idempotente: si llega dos veces el mismo `macro_transaction_id`, no procesar dos veces
- Log de cada webhook recibido para auditoría
---
## Sección 3 — Panel de administración
### Super admin (role=1) — nuevas secciones
**A. Conceptos de pago**
- Listado con filtros por tipo y estado (activo/inactivo)
- Crear / editar: nombre, descripción, monto, tipo, temporada, activo
- Desactivar un concepto no elimina ni afecta pagos ya realizados
**B. Sanciones**
- Formulario: buscar jugador → seleccionar club → ingresar motivo, descripción, fecha → asociar concepto de pago tipo `multa`
- Listado con estado de pago (pendiente / saldada); filtros por club, jugador, estado, fecha
**C. Tienda**
- CRUD de productos: nombre, descripción, precio, stock, imagen, activo
- Listado de órdenes con gestión de estado:
- `pagado` → "Marcar listo para retirar" → email automático al comprador
- `listo_retiro` → "Marcar como retirado"
- Filtros por estado y fecha
**D. Transacciones / Recaudación**
- Tabla de todos los `pagos` con filtros: estado, tipo de concepto, rango de fechas
- Totales agrupados por tipo de concepto
- Exportar CSV para conciliación con la plataforma de Macro
### Admin de club (role=2) — nuevas secciones
**A. Sanciones del club**
- Lista de sanciones de los jugadores de su club
- Estado de cada sanción (pendiente / saldada)
- Botón para pagar una sanción pendiente (el club abona, no el jugador)
**B. Inscripciones del club**
- Conceptos de inscripción de equipo disponibles para la temporada vigente
- Botón para pagar inscripción de equipo
---
## Sección 4 — Vistas del usuario
### Jugador registrado (`/panel-usuario`)
Se agregan dos bloques al panel existente:
**A. Mis pagos pendientes**
- Lista de cobros asignados al jugador: inscripción anual, sanciones
- Por cada uno: concepto, monto, estado, botón "Pagar ahora"
- Si ya está pagado: fecha + link al comprobante
**B. Historial de pagos**
- Todos los pagos realizados (inscripciones, sanciones, compras en tienda)
- Filtro por estado y fecha
### Tienda pública (`/tienda`)
- Accesible desde la navbar para todos (registrados y anónimos)
- Catálogo: productos activos con stock > 0
- Carrito en sesión (sin necesidad de cuenta)
- Checkout: formulario con nombre + email → pago con Macro
- Comprobante por email con información de retiro en sede
- Si el usuario tiene cuenta: la orden se vincula a su `user_id` y aparece en su historial
### Invitación a registrarse (opcional, no obligatorio)
En el checkout anónimo se muestra: *"¿Tenés cuenta? Ingresá para guardar tu historial de compras."*
---
## Sección 5 — Consideraciones técnicas y testing
### Pendientes a confirmar con Macro al recibir credenciales
- URL del endpoint para generar transacción / token de pago
- Formato exacto del webhook (campos, firma, algoritmo de verificación — probablemente HMAC-SHA256)
- Si existe **entorno sandbox** para pruebas (solicitarlo explícitamente)
- Si el botón es JS embebido, iframe, o redirect
### Estructura de clases nuevas en Laravel
```
app/
Services/
MacroService.php ← toda la comunicación con Macro (stub hasta recibir credenciales)
PagoService.php ← crea pagos, dispara eventos post-pago
Events/
PagoConfirmado.php
Listeners/
EnviarComprobantePago.php
MarcarJugadorInscripto.php
MarcarSancionSaldada.php
ActualizarOrdenTienda.php
Jobs/
CancelarPagosPendientesVencidos.php ← cron diario
Http/Controllers/
CheckoutController.php
MacroWebhookController.php
TiendaController.php
Admin/ConceptoPagoController.php
Admin/SancionAdminController.php
Admin/ProductoController.php
Admin/OrdenTiendaController.php
Admin/RecaudacionController.php
```
### Variables de entorno necesarias
```env
MACRO_MERCHANT_ID=
MACRO_API_KEY=
MACRO_SECRET=
MACRO_WEBHOOK_SECRET=
MACRO_ENV=sandbox # cambiar a "production" al salir a producción
MACRO_SUCCESS_URL="${APP_URL}/pagos/exitoso"
MACRO_FAILURE_URL="${APP_URL}/pagos/fallido"
MACRO_WEBHOOK_URL="${APP_URL}/api/macro/webhook"
```
### Plan de testing en dos etapas
**Etapa 1 — Sin credenciales Macro (desarrollo inmediato)**
- Tests unitarios del modelo de datos: crear pagos, sanciones, órdenes
- Tests de eventos post-pago con `MacroService` mockeado
- Tests del webhook con payload simulado y firma fake
- Tests de las vistas admin (CRUD de conceptos, productos, sanciones)
**Etapa 2 — Con Macro sandbox**
- Pruebas end-to-end con tarjeta de prueba en sandbox
- Verificar que el webhook llega y el estado se actualiza
- Verificar idempotencia (enviar webhook dos veces → mismo resultado)
- Verificar email de comprobante
### Orden de implementación recomendado
1. Migraciones + modelos (`ConceptoPago`, `Pago`, `Sancion`, `Producto`, `OrdenTienda`, `OrdenItem`)
2. `MacroService` con métodos stub
3. Panel admin: conceptos de pago → sanciones → tienda → reportes/recaudación
4. Panel usuario: pagos pendientes + historial
5. Tienda pública: catálogo + carrito + checkout
6. Webhook receptor + eventos post-pago + job de cancelación
7. Reemplazar stubs con implementación real de Macro al recibir credenciales
---
## Inscripción de jugadores — detalle adicional ✅
### Dos tipos de inscripción por temporada
La temporada abarca Torneo Apertura + Clausura. Hay jugadores que se incorporan a mitad de año y solo deben pagar una inscripción reducida. Por lo tanto, el super admin crea **dos conceptos** por temporada:
| Concepto | Tipo | Ejemplo precio | Aplica a |
|---|---|---|---|
| Inscripción Anual Completa 2026 | `inscripcion_jugador` | $X | Jugadores desde Apertura |
| Inscripción Reducida 2026 (Clausura) | `inscripcion_jugador` | $Y | Jugadores que ingresan a mitad de año |
### Generación masiva de deudas
En el panel admin (super admin), sección **Conceptos de pago**, se agrega una acción:
**"Generar deuda masiva"**
1. Super admin selecciona el concepto (ej: "Inscripción Anual Completa 2026")
2. Sistema muestra todos los jugadores activos que **no tienen ya ese concepto generado** para esa temporada
3. Super admin puede desmarcar jugadores individuales (ej: los que van a recibir la inscripción reducida)
4. Confirma → sistema crea un registro `pagos` (estado=pendiente) por cada jugador seleccionado
5. Cada jugador ve la deuda en su panel usuario
Para los jugadores que se incorporan a mitad de año, el admin repite el proceso con el concepto "Inscripción Reducida".
**Regla de negocio:** Un jugador no puede tener dos pagos pendientes del mismo concepto simultáneamente (se valida antes de generar).
## Preguntas abiertas
- ¿Tiene Macro un entorno sandbox para desarrollo? (pendiente confirmar con Diego Dallanora)
- ¿Qué campos exactos envía Macro en el webhook? (se confirma al recibir credenciales)