Looking to hire Laravel developers? Try LaraJobs

laravel-multi-payment-gateway maintained by heccubernny

Description
A Laravel package that acts as a multiple payment gateway API package, supporting Monnify, Stripe, Paystack, XpressPay, Upperlink, and Flutterwave.
Last update
2026/06/20 18:04 (dev-main)
License
Links
Downloads
0

Comments
comments powered by Disqus

Laravel Multi-Payment Gateway Package

A modern, robust, and extensible Laravel package designed to handle integrations for multiple payment gateways with runtime driver switching.

Supported Gateways

  • Stripe
  • Paystack
  • Flutterwave
  • Monnify
  • XpressPayments (XpressPay)
  • Upperlink (Paygate)

Key Design Patterns & Features

  • Manager Design Pattern: Leverages Laravel's native driver manager structure to resolve gateway drivers at runtime.
  • Dynamic Gateway Switching: Easily swap gateways inline using PaymentGateway::driver('gateway_name').
  • Decoupled Architecture: Fires standard Laravel events (PaymentSuccessful & PaymentFailed) upon transaction validation. The parent application simply listens to these events to credit or update user transactions.
  • Race Condition Prevention: Built-in webhook safety sleep delays ensure server-to-server notifications don't conflict with redirect callback processing.

Installation

You can install the package via Composer from Packagist:

composer require heccubernny/laravel-multi-payment-gateway

The Service Provider will be auto-discovered by Laravel. If you have auto-discovery disabled, manually register it in config/app.php (or bootstrap/providers.php on Laravel 11+):

'providers' => [
    PaymentGateway\MultiPayment\PaymentGatewayServiceProvider::class,
]

Configuration

Publish the config file:

php artisan vendor:publish --provider="PaymentGateway\MultiPayment\PaymentGatewayServiceProvider" --tag="payment-gateway-config"

Configure your credentials in .env. An example .env.example file is included in the package.


Usage

1. Initialization Payloads

When initiating checkouts, different gateways require specific formats for amounts and metadata.

For Upperlink (Paygate)

  • Amount: Must be in whole Naira, rounded down or to 2 decimal places.
  • Metadata: Pass stringified JSON using the meta key.
  • Success Check: Redirect to checkout URL on response code "200".
use PaymentGateway\MultiPayment\Facades\PaymentGateway;

$payload = [
    'amount' => floor($amountInNaira), // e.g. 5000
    'merchantId' => config('payment-gateway.gateways.upperlink.ref'),
    'meta' => json_encode([
        'user_id' => 1,
        'payment_id' => 2,
    ]),
    'redirectUrl' => config('payment-gateway.gateways.upperlink.redirect_url'),
];

$response = PaymentGateway::driver('upperlink')->initializeTransaction($payload);

if (isset($response['code']) && $response['code'] === '200') {
    $checkoutUrl = $response['data']['checkOutUrl'];
    return redirect()->away($checkoutUrl);
}

For XpressPayments (XpressPay)

  • Amount: Must be in whole Naira, formatted as a string.
  • Metadata: Array of name-value dictionaries.
  • Success Check: Redirect to payment URL on responseCode "00".
use PaymentGateway\MultiPayment\Facades\PaymentGateway;

$payload = [
    'amount' => (string) $amountInNaira, // e.g. "5000"
    'merchantId' => config('payment-gateway.gateways.xpresspay.merchant_id'),
    'metaData' => [
        ['name' => 'user_id', 'value' => '1'],
        ['name' => 'payment_id', 'value' => '2'],
    ],
    'callbackUrl' => config('payment-gateway.gateways.xpresspay.redirect_url'),
];

$response = PaymentGateway::driver('xpresspay')->initializeTransaction($payload);

if (isset($response['responseCode']) && $response['responseCode'] === '00') {
    $paymentUrl = $response['data']['paymentUrl'];
    return redirect()->away($paymentUrl);
}

2. Manual Verification

You can manually verify a transaction reference using:

$reference = 'TX-123456';

// For Default Gateway
$result = PaymentGateway::verifyTransaction($reference);

// For Specific Gateway
$result = PaymentGateway::driver('upperlink')->verifyTransaction($reference);

3. Dynamic Configuration Overrides

If your application uses dynamic database-driven credentials (e.g. multi-tenancy), you can dynamically override configuration options on the fly using the withConfig() or setConfig() methods:

use PaymentGateway\MultiPayment\Facades\PaymentGateway;

$customConfig = [
    'api_key' => 'XP_SEC_LIVE_DynamicKey123',
    'merchant_id' => 'XP_MERCH_DynamicId456'
];

$response = PaymentGateway::driver('xpresspay')
    ->withConfig($customConfig)
    ->initializeTransaction($payload);

You can pass any custom or optional parameters via the $payload array directly to initializeTransaction(), and the driver will forward it:

$payload = [
    'amount' => '5000',
    'merchantId' => 'XP_MERCH_12345',
    'currency' => 'NGN', // Optional parameter passed dynamically
    'custom_channel' => 'card', // Optional parameter
    'metaData' => [
        ['name' => 'user_id', 'value' => '1'],
    ]
];

4. Exception Handling

The package handles gateway connections and configuration issues gracefully. You can intercept exceptions thrown by the package for precise error handling:

  • PaymentGateway\MultiPayment\Exceptions\InvalidConfigurationException: Thrown if mandatory config keys are missing.
  • PaymentGateway\MultiPayment\Exceptions\PaymentApiException: Thrown for HTTP-level connection failures or gateway API errors (HTTP codes >= 400).
use PaymentGateway\MultiPayment\Exceptions\InvalidConfigurationException;
use PaymentGateway\MultiPayment\Exceptions\PaymentApiException;

try {
    $result = PaymentGateway::driver('xpresspay')->initializeTransaction($payload);
} catch (InvalidConfigurationException $e) {
    // Config not set up correctly in .env
    Log::error("Configuration error: " . $e->getMessage());
} catch (PaymentApiException $e) {
    // API error (e.g., unauthorized or bad request)
    Log::error("Gateway error: " . $e->getMessage() . " (Code: " . $e->getCode() . ")");
}

Callbacks and Webhooks

The package includes preconfigured endpoints and controller mappings:

Endpoint Method Purpose
/api/payment/xpresspay-callback GET/POST XpressPay synchronous redirect callback
/api/payment/upperlink-callback GET/POST Upperlink synchronous redirect callback
/api/payment/xpresspay-webhook POST XpressPay asynchronous server-to-server webhook
/api/payment/upperlink-webhook POST Upperlink asynchronous server-to-server webhook

Event-Driven Local Crediting

When a webhook or callback successfully verifies a transaction, the package fires the PaymentSuccessful event. You can create a Listener in your host application to credit transactions locally.

Example Listener Setup

  1. Register your listener in app/Providers/EventServiceProvider.php:
use PaymentGateway\MultiPayment\Events\PaymentSuccessful;
use App\Listeners\CreditLocalPayment;

protected $listen = [
    PaymentSuccessful::class => [
        CreditLocalPayment::class,
    ],
];
  1. Implement App\Listeners\CreditLocalPayment.php:
namespace App\Listeners;

use PaymentGateway\MultiPayment\Events\PaymentSuccessful;
use App\Models\Transaction;
use App\Models\User;

class CreditLocalPayment
{
    public function handle(PaymentSuccessful $event)
    {
        $gateway = $event->gateway;       // e.g. 'xpresspay' or 'upperlink'
        $reference = $event->reference;   // e.g. transaction reference string
        $metadata = $event->metadata;     // Array containing ['user_id' => 1, 'payment_id' => 2]
        
        // Process crediting logic
        $userId = $metadata['user_id'] ?? null;
        $paymentId = $metadata['payment_id'] ?? null;

        $transaction = Transaction::where('reference', $reference)->first();

        if ($transaction && !$transaction->isCompleted()) {
            $transaction->update([
                'status' => 'completed',
                'gateway' => $gateway,
                'metadata' => json_encode($metadata)
            ]);
            
            // Credit student wallet or application fee
            // User::find($userId)->creditWallet($transaction->amount);
        }
    }
}