jamesmosq/pasarelas-pago-simulador

Simulador de pasarelas de pago para Laravel — prueba Wompi, PayU y pasarelas genéricas en local sin credenciales reales

Maintainers

Package info

github.com/jamesmosq/pasarelas-pago-simulador-demo

pkg:composer/jamesmosq/pasarelas-pago-simulador

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-05-31 06:20 UTC

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=local o APP_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