← Back to fiachehr.ir
Fiachehr · Package docs

Laravel Pardakht

Laravel payment integration for Iran: Bank Mellat (SOAP), Mabna / Sepehr (REST), and ZarinPal (REST). Includes value objects, gateway manager, facade, optional DB persistence of transactions, sandbox switches, and extension points for custom drivers. Laravel 10–12.

Features

  • SOLID-oriented design: contracts, repositories, factory/manager pattern.
  • Strongly typed requests/responses (PaymentRequest, VerificationRequest, …).
  • Switch default gateway via config/env or pass driver name per call.
  • Optional automatic persistence of attempts/results to pardakht_transactions.
  • Sandbox flags per gateway for staging.

Requirements

  • PHP 8.1+ (Laravel 12 needs PHP 8.2+)
  • Laravel 10 / 11 / 12
  • ext-soap — required for Mellat
  • ext-json
  • Guzzle (declared dependency)

Gateways

DriverProviderProtocol
mellatBank MellatSOAP
mabnaMabna Card (Sepehr)REST
zarinpalZarinPalREST

Installation

composer require fiachehr/laravel-pardakht
# All publishable assets (config + migrations)
php artisan vendor:publish --provider="Fiachehr\Pardakht\PardakhtServiceProvider"

Or selectively:

php artisan vendor:publish --tag=pardakht-config
php artisan vendor:publish --tag=pardakht-migrations
php artisan migrate

Registers PardakhtServiceProvider and facade alias Pardakht.

Configuration file

config/pardakht.php (illustrative structure):

return [
    'default' => env('PARDAKHT_DEFAULT_GATEWAY', 'mellat'),

    'gateways' => [
        'mellat' => [
            'driver' => 'mellat',
            'terminal_id' => env('MELLAT_TERMINAL_ID'),
            'username' => env('MELLAT_USERNAME'),
            'password' => env('MELLAT_PASSWORD'),
            'callback_url' => env('MELLAT_CALLBACK_URL'),
            'sandbox' => env('MELLAT_SANDBOX', false),
        ],
        'mabna' => [
            'driver' => 'mabna',
            'terminal_id' => env('MABNA_TERMINAL_ID'),
            'merchant_id' => env('MABNA_MERCHANT_ID'),
            'password' => env('MABNA_PASSWORD'),
            'callback_url' => env('MABNA_CALLBACK_URL'),
            'sandbox' => env('MABNA_SANDBOX', false),
        ],
        'zarinpal' => [
            'driver' => 'zarinpal',
            'merchant_id' => env('ZARINPAL_MERCHANT_ID'),
            'callback_url' => env('ZARINPAL_CALLBACK_URL'),
            'sandbox' => env('ZARINPAL_SANDBOX', false),
            'description' => env('ZARINPAL_DESCRIPTION', 'Payment via ZarinPal'),
        ],
    ],

    'store_transactions' => env('PARDAKHT_STORE_TRANSACTIONS', true),
    'transaction_table' => 'pardakht_transactions',
    // ... currency options in full config file
];

Set PARDAKHT_STORE_TRANSACTIONS=false (or override in config) to skip DB persistence.

Environment variables

PARDAKHT_DEFAULT_GATEWAY=mellat

# Mellat
MELLAT_TERMINAL_ID=
MELLAT_USERNAME=
MELLAT_PASSWORD=
MELLAT_CALLBACK_URL=https://yoursite.com/payment/callback
MELLAT_SANDBOX=false

# Mabna / Sepehr
MABNA_TERMINAL_ID=
MABNA_MERCHANT_ID=
MABNA_PASSWORD=
MABNA_CALLBACK_URL=https://yoursite.com/payment/callback
MABNA_SANDBOX=false

# ZarinPal
ZARINPAL_MERCHANT_ID=
ZARINPAL_CALLBACK_URL=https://yoursite.com/payment/callback
ZARINPAL_SANDBOX=false
ZARINPAL_DESCRIPTION="Payment via ZarinPal"
Amounts are in Rials as consumed by the gateways. Use sandbox credentials from BankTest.ir / ZarinPal sandbox while developing.

Payment request

use Fiachehr\Pardakht\Facades\Pardakht;
use Fiachehr\Pardakht\ValueObjects\PaymentRequest;

public function pay()
{
    $paymentRequest = new PaymentRequest(
        amount: 100000,
        orderId: 'ORDER-' . uniqid(),
        callbackUrl: route('payment.callback'),
        description: 'Order payment',
        mobile: '09123456789',
        email: 'user@example.com',
        metadata: [
            'user_id' => auth()->id(),
            'product_id' => 5,
        ],
    );

    try {
        $response = Pardakht::request($paymentRequest);
        // Or: Pardakht::request($paymentRequest, 'zarinpal');

        if ($response->isSuccessful()) {
            session(['payment_tracking_code' => $response->trackingCode]);

            return redirect($response->getPaymentUrl());
        }

        return back()->with('error', 'Gateway did not return a payment URL.');
    } catch (\Fiachehr\Pardakht\Exceptions\GatewayException $e) {
        report($e);

        return back()->with('error', $e->getMessage());
    }
}

Verification (callback)

use Fiachehr\Pardakht\Facades\Pardakht;
use Fiachehr\Pardakht\ValueObjects\VerificationRequest;
use Illuminate\Http\Request;

public function callback(Request $request)
{
    $trackingCode = session('payment_tracking_code');

    if (! $trackingCode) {
        return redirect()->route('payment.failed')
            ->with('error', 'Session expired or invalid payment session.');
    }

    $verificationRequest = new VerificationRequest(
        trackingCode: $trackingCode,
        gatewayData: $request->all(),
    );

    try {
        $response = Pardakht::verify($verificationRequest);
        // Or: Pardakht::verify($verificationRequest, 'mellat');

        if ($response->isSuccessful()) {
            // Fulfill order, grant access, etc.
            // $response->referenceId, $response->amount, $response->transactionId

            session()->forget('payment_tracking_code');

            return view('payment.success', [
                'referenceId' => $response->referenceId,
                'cardNumber' => $response->getMaskedCardNumber(),
                'amount' => $response->amount,
                'transactionId' => $response->transactionId,
            ]);
        }

        return view('payment.failed', ['message' => 'Verification was not successful.']);
    } catch (\Fiachehr\Pardakht\Exceptions\GatewayException $e) {
        report($e);

        return view('payment.failed', [
            'message' => $e->getMessage(),
            'code' => $e->getGatewayCode(),
        ]);
    }
}

Exception handling

Catch Fiachehr\Pardakht\Exceptions\GatewayException for provider-level failures. Useful accessors:

  • getMessage() — human-readable message
  • getGatewayName() — which driver failed
  • getGatewayCode() — provider error code when available

Multiple gateways

$drivers = Pardakht::available();
// ['mellat', 'mabna', 'zarinpal'] (depending on config)

$mellat = Pardakht::gateway('mellat');
$response = $mellat->request($paymentRequest);

Let users pick a gateway in UI, then pass the key into request() / verify().

Transactions & repository

When store_transactions is true, the manager receives a repository implementation bound in the service provider.

use Fiachehr\Pardakht\Contracts\TransactionRepositoryInterface;

public function __construct(
    protected TransactionRepositoryInterface $transactions
) {}

public function history()
{
    $ok = $this->transactions->getSuccessful();
    $bad = $this->transactions->getFailed();
    $one = $this->transactions->findByTrackingCode($code);
    $byOrder = $this->transactions->findByOrderId($orderId);
}

Eloquent model

use Fiachehr\Pardakht\Models\Transaction;

Transaction::gateway('mellat')->successful()->latest()->get();
Transaction::pending()->get();
Transaction::whereDate('created_at', today())->successful()->get();

$t = Transaction::find(1);
$t->isSuccessful();

Custom gateway

use Fiachehr\Pardakht\Facades\Pardakht;
use Fiachehr\Pardakht\Gateways\AbstractGateway;
use Fiachehr\Pardakht\ValueObjects\PaymentRequest;
use Fiachehr\Pardakht\ValueObjects\PaymentResponse;
use Fiachehr\Pardakht\ValueObjects\VerificationRequest;
use Fiachehr\Pardakht\ValueObjects\VerificationResponse;

class CustomGateway extends AbstractGateway
{
    public function getName(): string
    {
        return 'custom';
    }

    public function request(PaymentRequest $request): PaymentResponse
    {
        // Call remote API, return PaymentResponse
    }

    public function verify(VerificationRequest $request): VerificationResponse
    {
        // Parse gateway POST/GET, return VerificationResponse
    }

    protected function validateConfig(): void
    {
        // Assert required config keys exist
    }
}

Pardakht::extend('custom', CustomGateway::class);

Register extensions in a service provider boot() method so they exist before handling HTTP traffic.

Testing

composer test
composer test-coverage

vendor/bin/phpunit --testsuite=Unit
vendor/bin/phpunit --testsuite=Feature

Includes coverage for value objects, gateway manager, transactions, exceptions, facade, and repositories.

Security

  • Never commit merchant/terminal secrets; use .env and locked-down CI secrets.
  • Force HTTPS for callback URLs in production.
  • Turn off sandbox flags in production.
  • Log and monitor failed verifications; reconcile with gateway panels.
  • Validate callback payloads: check signature rules per bank documentation.

FAQ

Change default gateway?

Set PARDAKHT_DEFAULT_GATEWAY or pass the second argument to request() / verify().

Disable DB writes?

'store_transactions' => false in config.

User stays on bank page?

Ensure getPaymentUrl() redirect is returned; check for gateway errors before redirect.