Ahora si(?
This commit is contained in:
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 (2–5s), 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)
|
||||
Reference in New Issue
Block a user