jamesmosq / pasarelas-pago-simulador
Simulador de pasarelas de pago para Laravel — prueba Wompi, PayU y pasarelas genéricas en local sin credenciales reales
Package info
github.com/jamesmosq/pasarelas-pago-simulador-demo
pkg:composer/jamesmosq/pasarelas-pago-simulador
Requires
- php: ^8.2
- illuminate/console: ^11.0|^13.0
- illuminate/database: ^11.0|^13.0
- illuminate/http: ^11.0|^13.0
- illuminate/mail: ^11.0|^13.0
- illuminate/queue: ^11.0|^13.0
- illuminate/routing: ^11.0|^13.0
- illuminate/support: ^11.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^11.0|^12.0
This package is auto-updated.
Last update: 2026-05-31 06:22:35 UTC
README
El Mailtrap de los pagos. Paquete Laravel para simular Wompi, PayU y pasarelas genéricas en local con webhooks reales, números mágicos y panel de administración.
Instalación
composer require jamesmosq/pasarelas-pago-simulador --dev
El paquete se registra automáticamente vía Laravel Package Auto-Discovery. Publica la config y corre las migraciones:
php artisan vendor:publish --tag=payment-simulator-config
php artisan migrate
php artisan payment:seed-products # opcional: 6 productos de prueba
Requisitos: PHP 8.2+, Laravel 11+, cualquier driver de base de datos, queue driver database o redis.
Configuración básica
Añade a tu .env:
PAYMENT_DRIVER=simulator PAYMENT_WEBHOOK_URL=https://tu-app.test/webhooks/payment PAYMENT_MERCHANT_NAME="Mi Comercio"
El paquete está bloqueado en producción. Si APP_ENV no es local o testing, el panel y el checkout devuelven 403 aunque PAYMENT_DRIVER=simulator esté configurado.
Uso básico
use JamesMosq\PaymentSimulator\Facades\PaymentSimulator; $checkoutUrl = PaymentSimulator::createPayment([ 'reference' => 'ORDER-' . $order->id, 'amount' => $order->total_in_cents, // centavos 'return_url' => route('orders.show', $order), 'webhook_url' => route('webhooks.payment'), 'customer_email' => $user->email, 'payment_method' => 'CARD', // CARD | PSE | NEQUI | DAVIPLATA | EFECTY | BALOTO ]); return redirect($checkoutUrl);
El usuario paga en el checkout del simulador y regresa a return_url. Tu webhook recibe el payload exacto de Wompi o PayU.
Tarjetas mágicas
| Número | Marca | Resultado | Razón de rechazo |
|---|---|---|---|
| 4242424242424242 | Visa | APPROVED | — |
| 4111111111111111 | Visa | APPROVED | — |
| 5555555555554444 | Mastercard | APPROVED | — |
| 378282246310005 | Amex | APPROVED | — |
| 4000000000000002 | Visa | DECLINED | INSUFFICIENT_FUNDS |
| 4000000000000069 | Visa | DECLINED | EXPIRED_CARD |
| 4000000000000119 | Visa | DECLINED | PROCESSING_ERROR |
| 4000000000009995 | Visa | DECLINED (permanente) | STOLEN_CARD |
| 4000000000000127 | Visa | DECLINED | SECURITY_VIOLATION |
| 4000000000000341 | Visa | DECLINED | LIMIT_EXCEEDED |
| 5200828282828210 | Mastercard | DECLINED | INSUFFICIENT_FUNDS |
| 4000000000003220 | Visa | 3DS — autenticado | — |
| 4000000000003063 | Visa | 3DS — fallido | AUTHENTICATION_FAILED |
| 4000000000003238 | Visa | 3DS — frictionless | — |
| 4000000000000259 | Visa | APPROVED + chargeback | Chargeback automático a los 30s |
Cualquier otro número: APPROVED por defecto.
Cédulas / NITs mágicos (PSE)
| Documento | Resultado | Razón |
|---|---|---|
| 1234567890 | APPROVED | — |
| 9001234567 | APPROVED | — |
| 9876543210 | DECLINED | ACCOUNT_NOT_FOUND |
| 1111111111 | PENDING | BANK_NOT_AVAILABLE |
| 2222222222 | DECLINED | INSUFFICIENT_FUNDS |
| 3333333333 | DECLINED | BLOCKED_ACCOUNT |
Cualquier otro documento: APPROVED por defecto.
Teléfonos mágicos (NEQUI / Daviplata)
| Teléfono | Resultado | Delay | Razón |
|---|---|---|---|
| 3001234567 | APPROVED | 5s | — |
| 3009999999 | DECLINED | 3s | USER_REJECTED |
| 3008888888 | DECLINED | 120s | TIMEOUT |
| 3007777777 | DECLINED | 3s | INSUFFICIENT_FUNDS |
| 3006666666 | DECLINED | 3s | MOBILE_NOT_ACTIVE |
Cualquier otro teléfono: APPROVED en ~8s por defecto.
Métodos de pago
| Método | Descripción | Monto mínimo |
|---|---|---|
| CARD | Tarjeta crédito/débito con cuotas y 3DS | $1.000 COP |
| PSE | Débito bancario (resolución asíncrona) | $1.000 COP |
| NEQUI | Billetera digital (polling SSE) | $1.000 COP |
| DAVIPLATA | Billetera digital (polling SSE) | $1.000 COP |
| EFECTY | Efectivo (referencia generada) | $2.000 COP |
| BALOTO | Bancolombia Collect | $2.000 COP |
Cuotas
Por defecto: 1, 2, 3, 6, 12, 24, 36. Tasa simulada: 1.8% mensual.
PaymentSimulator::createPayment([ 'reference' => 'ORDER-1', 'amount' => 600000, 'installments' => 6, 'return_url' => '...', ]);
Tokenización
// 1. Guardar tarjeta al pagar PaymentSimulator::createPayment([ 'reference' => 'ORDER-1', 'amount' => 50000, 'return_url' => '...', 'metadata' => ['save_card' => true], ]); // 2. Recuperar el token $token = PaymentSimulator::getStatus('ORDER-1')['token']; // "sim_tok_xxxxxxxxxxxxxxxxxxxx" // 3. Cobros futuros sin checkout PaymentSimulator::chargeToken($token, [ 'reference' => 'ORDER-2', 'amount' => 50000, 'return_url' => '...', ]);
Pre-autorización y captura
$url = PaymentSimulator::authorize([ 'reference' => 'HOTEL-1', 'amount' => 200000, 'return_url' => '...', ]); PaymentSimulator::capture('HOTEL-1'); // Captura total PaymentSimulator::capture('HOTEL-1', 150000); // Captura parcial
Las pre-autorizaciones no capturadas expiran automáticamente (scheduler diario).
Void vs Refund
| Acción | Cuándo | Resultado |
|---|---|---|
| Void | Mismo día, antes de liquidar | VOIDED |
| Refund | Después de liquidación (≥1 día) | REFUNDED |
PaymentSimulator::void('ORDER-1'); PaymentSimulator::refund('ORDER-1'); PaymentSimulator::refund('ORDER-1', 30000); // parcial
Webhooks
Los payloads replican exactamente el formato de cada pasarela:
Wompi:
{
"event": "transaction.updated",
"data": {
"transaction": {
"id": "uuid",
"reference": "ORDER-1",
"status": "APPROVED",
"amount_in_cents": 50000
}
},
"environment": "test",
"signature": { "checksum": "sha256hex" }
}
PayU:
{
"state_pol": "4",
"reference_sale": "ORDER-1",
"value": "500.00",
"currency": "COP",
"sign": "md5hash"
}
Reintentos automáticos cuando tu endpoint responde con un código no-2xx:
| Intento | Delay |
|---|---|
| 1 | 5 minutos |
| 2 | 15 minutos |
| 3 | 1 hora |
| 4 | 6 horas |
| 5 | 24 horas |
Panel de administración
Disponible en http://tu-app.test/payment-simulator (solo en entorno local).
| Sección | URL |
|---|---|
| Transacciones | /payment-simulator |
| Inspector | /payment-simulator/transactions/{id} |
| Links de pago | /payment-simulator/payment-links |
| Tokens | /payment-simulator/tokens |
| Productos | /payment-simulator/products |
| Reportes CSV | /payment-simulator/reports |
| Estadísticas | /payment-simulator/stats |
El inspector permite aprobar, rechazar, anular, reembolsar, capturar, forzar chargeback, reenviar webhook y ver el audit log en tiempo real.
Comandos Artisan
Acciones manuales:
php artisan payment:approve {reference}
php artisan payment:reject {reference} {--reason=PROCESSING_ERROR}
php artisan payment:void {reference}
php artisan payment:refund {reference} {--amount=}
php artisan payment:capture {reference} {--amount=}
php artisan payment:chargeback {reference} {--reason=FRAUD_DISPUTE}
php artisan payment:charge-token {token} --amount= --reference=
Schedulers (se registran automáticamente vía ServiceProvider):
php artisan payment:expire-pending # cada minuto php artisan payment:settle-transactions # cada hora php artisan payment:expire-pre-auths # diario php artisan payment:retry-webhooks # cada 5 minutos
Mantenimiento:
php artisan payment:list {--status=} {--gateway=} {--limit=20}
php artisan payment:clear {--days=7} {--force}
php artisan payment:seed-products
php artisan payment:reset-fraud
Testing
Setup
use JamesMosq\PaymentSimulator\Facades\PaymentSimulator; use JamesMosq\PaymentSimulator\Testing\InteractsWithPaymentSimulator; class OrderPaymentTest extends TestCase { use RefreshDatabase; use InteractsWithPaymentSimulator; // resetea estado entre tests protected function setUp(): void { parent::setUp(); Http::fake(['*' => Http::response('OK', 200)]); Queue::fake(); } }
Forzar resultados
$this->approveNextPayment(); // todos aprobados $this->declineNextPayment('INSUFFICIENT_FUNDS'); // todos rechazados PaymentSimulator::approveReference('ORDER-42'); // solo esa referencia PaymentSimulator::rejectReference('ORDER-43', 'EXPIRED_CARD'); PaymentSimulator::reset(); // limpiar estado
Aserciones
// Estado de la transacción PaymentSimulator::assertApproved('ORDER-1'); PaymentSimulator::assertDeclined('ORDER-1'); PaymentSimulator::assertDeclinedWith('ORDER-1', 'INSUFFICIENT_FUNDS'); PaymentSimulator::assertAuthorized('ORDER-1'); PaymentSimulator::assertCaptured('ORDER-1'); PaymentSimulator::assertRefunded('ORDER-1'); PaymentSimulator::assertVoided('ORDER-1'); PaymentSimulator::assertChargeback('ORDER-1'); PaymentSimulator::assertExpired('ORDER-1'); // Webhooks PaymentSimulator::assertWebhookSent('ORDER-1'); PaymentSimulator::assertWebhookNotSent('ORDER-1'); PaymentSimulator::assertWebhookAttempts('ORDER-1', 2); PaymentSimulator::assertWebhookPayloadContains('ORDER-1', [ 'data.transaction.status' => 'APPROVED', ]); // 3DS, cuotas, tokens, email PaymentSimulator::assertThreeDsAuthenticated('ORDER-1'); PaymentSimulator::assertInstallments('ORDER-1', 6); PaymentSimulator::assertCardTokenCreated('ORDER-1'); PaymentSimulator::assertReceiptEmailSent('ORDER-1'); // Audit log PaymentSimulator::assertEventLogged('ORDER-1', 'status_changed'); PaymentSimulator::assertActorWas('ORDER-1', 'artisan'); // Inspección $tx = PaymentSimulator::find('ORDER-1'); $events = PaymentSimulator::events('ORDER-1');
Ejemplo de test completo
public function test_order_is_confirmed_after_payment(): void { $this->approveNextPayment(); $order = Order::factory()->create(); $this->post('/orders/' . $order->id . '/pay'); PaymentSimulator::assertApproved('ORDER-' . $order->id); PaymentSimulator::assertWebhookSent('ORDER-' . $order->id); $this->assertDatabaseHas('orders', ['id' => $order->id, 'status' => 'paid']); } public function test_order_stays_pending_on_declined_payment(): void { $this->declineNextPayment('INSUFFICIENT_FUNDS'); $order = Order::factory()->create(); $this->post('/orders/' . $order->id . '/pay'); PaymentSimulator::assertDeclinedWith('ORDER-' . $order->id, 'INSUFFICIENT_FUNDS'); $this->assertDatabaseHas('orders', ['id' => $order->id, 'status' => 'pending']); }
Variables de entorno
| Variable | Default | Descripción |
|---|---|---|
PAYMENT_DRIVER |
simulator |
Driver: simulator, wompi, payu |
PAYMENT_SIMULATOR_URL |
/payment-simulator |
URL base del panel |
PAYMENT_WEBHOOK_URL |
null |
URL por defecto para webhooks |
PAYMENT_WEBHOOK_DELAY |
0 |
Delay artificial antes de enviar (segundos) |
PAYMENT_WEBHOOK_RETRY |
true |
Habilitar reintentos automáticos |
PAYMENT_AUTO_RESOLVE |
null |
Resolución automática: approved/declined |
PAYMENT_LINK_EXPIRY |
1800 |
Vida del link de checkout en segundos |
PAYMENT_PSE_DELAY |
3 |
Segundos de procesamiento PSE simulado |
PAYMENT_NEQUI_TIMEOUT |
120 |
Timeout NEQUI en segundos |
PAYMENT_SETTLEMENT_DELAY |
1 |
Días hasta liquidación (habilita refunds) |
PAYMENT_CAPTURE_DEADLINE |
7 |
Días límite para capturar pre-auths |
PAYMENT_SEND_RECEIPT |
true |
Enviar email de comprobante |
PAYMENT_MERCHANT_NAME |
"Mi Comercio (Simulado)" |
Nombre en el checkout |
PAYMENT_FRAUD_CARD_MAX |
3 |
Intentos fallidos antes de bloquear tarjeta |
PAYMENT_FRAUD_IP_MAX |
5 |
Intentos fallidos antes de bloquear IP |
WOMPI_PUBLIC_KEY |
— | Clave pública Wompi (producción) |
WOMPI_PRIVATE_KEY |
— | Clave privada Wompi |
WOMPI_INTEGRITY_KEY |
— | Clave de integridad Wompi |
PAYU_MERCHANT_ID |
— | ID de comercio PayU |
PAYU_API_KEY |
— | API Key PayU |
PAYU_API_LOGIN |
— | API Login PayU |
Cambiar a producción
Una sola línea en .env:
# Desarrollo PAYMENT_DRIVER=simulator # Producción con Wompi PAYMENT_DRIVER=wompi WOMPI_PUBLIC_KEY=pub_prod_xxx WOMPI_PRIVATE_KEY=prv_prod_xxx WOMPI_INTEGRITY_KEY=xxx # Producción con PayU PAYMENT_DRIVER=payu PAYU_MERCHANT_ID=xxx PAYU_API_KEY=xxx PAYU_API_LOGIN=xxx
El código de tu aplicación no cambia — PaymentSimulator::createPayment([...]) funciona igual con cualquier driver gracias al contrato PaymentGatewayContract.
Seguridad
- El panel solo es accesible con
APP_ENV=localoAPP_ENV=testing. Cualquier otro entorno retorna 403. - Los números mágicos de tarjeta son identificadores de test, no PANs reales.
- Valida siempre la firma del webhook en tu handler (SHA-256 para Wompi, MD5 para PayU).
Licencia
MIT