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
| Driver | Provider | Protocol |
|---|---|---|
mellat | Bank Mellat | SOAP |
mabna | Mabna Card (Sepehr) | REST |
zarinpal | ZarinPal | REST |
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"
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 messagegetGatewayName()— which driver failedgetGatewayCode()— 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
.envand 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.