# 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)
