377 lines
14 KiB
Markdown
377 lines
14 KiB
Markdown
# 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)
|