14 KiB
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):
- Motor de pagos — integración Macro base, modelo de datos, webhook
- Cobros institucionales — inscripciones, multas/sanciones
- 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 compradorlisto_retiro→ "Marcar como retirado"
- Filtros por estado y fecha
D. Transacciones / Recaudación
- Tabla de todos los
pagoscon 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_idy 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
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
MacroServicemockeado - 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
- Migraciones + modelos (
ConceptoPago,Pago,Sancion,Producto,OrdenTienda,OrdenItem) MacroServicecon métodos stub- Panel admin: conceptos de pago → sanciones → tienda → reportes/recaudación
- Panel usuario: pagos pendientes + historial
- Tienda pública: catálogo + carrito + checkout
- Webhook receptor + eventos post-pago + job de cancelación
- 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"
- Super admin selecciona el concepto (ej: "Inscripción Anual Completa 2026")
- Sistema muestra todos los jugadores activos que no tienen ya ese concepto generado para esa temporada
- Super admin puede desmarcar jugadores individuales (ej: los que van a recibir la inscripción reducida)
- Confirma → sistema crea un registro
pagos(estado=pendiente) por cada jugador seleccionado - 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)