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