Cómo ejecutar acciones automáticas al completar pagos, sin importar el proveedor
// En StripeController if ($result->success) { Payment::create([...]); Mail::to($customer)->send(...); Notification::send($admin, ...); Inventory::reduce(...); } // En RedsysController if ($result->success) { Payment::create([...]); // DUPLICADO Mail::to($customer)->send(...); // DUPLICADO Notification::send($admin, ...); // DUPLICADO Inventory::reduce(...); // DUPLICADO } // Y así con cada proveedor... 😫
Problemas: Código duplicado, difícil de mantener, mezcla lógica de negocio.
// En TODOS los controllers if ($result->success) { event(new PaymentCompleted( provider: PaymentProvider::STRIPE, result: $result, orderId: $orderId, amount: $amount, currency: 'EUR' )); } // ✨ Los listeners se ejecutan automáticamente: // → LogPaymentToDatabase // → SendConfirmationEmail // → SendAdminNotification // → UpdateInventory
Ventajas: Sin duplicación, fácil de mantener, separación de responsabilidades.
En Stripe, Redsys o PayPal
$result = $gateway->capture($paymentId);
event(new PaymentCompleted(...));
⚡ LogPaymentToDatabase (inmediato)
⚡ UpdateInventory (inmediato)
⏱️ SendConfirmationEmail (cola)
⏱️ SendAdminNotification (cola)
Página de éxito o error
Segundos después, los listeners asíncronos se ejecutan sin bloquear la respuesta
Encapsula toda la información de un pago completado, independiente del proveedor.
// app/Events/PaymentCompleted.php event(new PaymentCompleted( provider: PaymentProvider::STRIPE, // STRIPE, REDSYS, PAYPAL result: $paymentResult, // Resultado del pago orderId: 'ORDER-123', // ID de la orden amount: 99.99, // Cantidad pagada currency: 'EUR', // Moneda metadata: ['items' => [...]], // Datos adicionales customerEmail: 'cliente@example.com' // Email del cliente ));
Clases que se ejecutan automáticamente cuando el evento se dispara.
Guarda el pago en la base de datos. Se ejecuta inmediatamente porque es crítico.
Payment::create([ 'order_id' => $event->orderId, 'payment_id' => $event->result->paymentId, 'provider' => $event->provider, 'amount' => $event->amount, 'currency' => $event->currency, 'completed_at' => now(), ]);
Envía email de confirmación al cliente. Se ejecuta en background para no bloquear la respuesta.
Mail::to($event->customerEmail) ->send(new PaymentConfirmationMail($event)); // shouldQueue() retorna true // → Se ejecuta en cola (background)
Notifica al administrador (email, Slack, SMS, etc.). También en background.
// Puedes usar múltiples canales: // Email Mail::to($adminEmail)->send(...); // Slack Notification::route('slack', $webhook) ->notify(new PaymentNotification($event)); // SMS, WhatsApp, Discord, etc.
Actualiza inventario, activa suscripciones, reduce stock. Inmediato para evitar sobre-ventas.
$items = $event->metadata['items'] ?? []; foreach ($items as $item) { $product = Product::find($item['product_id']); if ($product) { $product->decrement('stock', $item['quantity']); } }
| Aspecto | ⚡ Síncrono | ⏱️ Asíncrono (Cola) |
|---|---|---|
| Ejecución | Inmediata, antes de responder | Background, después de responder |
| Bloquea respuesta | ✅ Sí | ❌ No |
| Velocidad | Ralentiza la respuesta | Respuesta inmediata |
| Uso recomendado | Operaciones críticas (BD, stock) | Emails, notificaciones, logs |
| Reintentos | No automáticos | ✅ Automáticos si falla |
| Ejemplos | LogPaymentToDatabase, UpdateInventory | SendEmail, SendNotification |
El mismo evento funciona para Stripe, Redsys, PayPal y cualquier proveedor futuro. No importa cómo se hizo el pago.
¿Quieres enviar a Discord? Crea un listener, regístralo. ¡Listo! Sin tocar controllers ni servicios.
Comenta una línea en AppServiceProvider para desactivar cualquier acción. Sin borrar código.
Event::fake() te permite testear fácilmente que los eventos se disparan correctamente.
Crea listeners para métricas, analytics, APM. Un solo lugar para todo el seguimiento.
Ejecuta acciones solo bajo ciertas condiciones (montos, usuarios, productos específicos).
php artisan make:listener NombreListener --event=PaymentCompleted
class NombreListener { public function handle(PaymentCompleted $event): void { // Tu código aquí // Acceso a datos: // $event->provider // $event->amount // $event->orderId // $event->customerEmail // $event->metadata } // Opcional: ejecutar en cola (background) public function shouldQueue(): bool { return true; } }
// app/Providers/AppServiceProvider.php Event::listen(PaymentCompleted::class, [ LogPaymentToDatabase::class, SendPaymentConfirmationEmail::class, SendAdminNotification::class, UpdateInventory::class, NombreListener::class, ← Tu nuevo listener ]);
Imaginemos que queremos enviar una notificación a Discord cuando un pago sea mayor a €100:
// app/Listeners/SendDiscordHighValueAlert.php namespace App\Listeners; use App\Events\PaymentCompleted; use Illuminate\Support\Facades\Http; class SendDiscordHighValueAlert { public function handle(PaymentCompleted $event): void { // Solo si el pago es >= 100€ if ($event->amount < 100) { return; } // Construir mensaje para Discord $webhook = config('services.discord.webhook'); $message = "💰 ¡Pago grande! €{$event->amount} via {$event->provider->value}"; // Enviar a Discord Http::post($webhook, [ 'content' => $message, 'embeds' => [[ 'title' => 'Nuevo Pago de Alto Valor', 'color' => 0x00ff00, 'fields' => [ [ 'name' => 'Cantidad', 'value' => "€{$event->amount}", 'inline' => true ], [ 'name' => 'Proveedor', 'value' => $event->provider->value, 'inline' => true ], [ 'name' => 'Order ID', 'value' => $event->orderId ], ] ]] ]); } public function shouldQueue(): bool { return true; // Ejecutar en background } }