sarkhanrasimoghlu / laravel-pasha-bank-payment
Laravel package for Pasha Bank ECOMM payment integration
Package info
github.com/Rasimoghlu/laravel-pasha-bank-payment
pkg:composer/sarkhanrasimoghlu/laravel-pasha-bank-payment
Requires
- php: ^8.3
- guzzlehttp/guzzle: ^7.8
- laravel/framework: ^12.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-04-13 13:58:12 UTC
README
A Laravel package for integrating with the Pasha Bank ECOMM payment system. Supports SMS payments, DMS (pre-authorization + capture), reversals, refunds, and end-of-business-day settlement out of the box.
Requirements
- PHP 8.3+
- Laravel 12.0+
- Guzzle 7.8+
- SSL client certificate (provided by Pasha Bank)
Installation
composer require sarkhanrasimoghlu/laravel-pasha-bank-payment
The service provider is auto-discovered. Publish the config and migration:
php artisan vendor:publish --tag=pasha-bank-config php artisan vendor:publish --tag=pasha-bank-migrations php artisan migrate
Configuration
Add these to your .env file:
PASHA_BANK_TERMINAL_ID=your-terminal-id PASHA_BANK_CERTIFICATE=/path/to/keystore.p12 PASHA_BANK_CERTIFICATE_PASSWORD=your-cert-password PASHA_BANK_PRIVATE_KEY=/path/to/key.pem PASHA_BANK_CA_CERTIFICATE=/path/to/PSroot.pem PASHA_BANK_SUCCESS_URL=https://yourapp.com/payment/success PASHA_BANK_ERROR_URL=https://yourapp.com/payment/error
| Key | Required | Description |
|---|---|---|
TERMINAL_ID |
Yes | Terminal ID provided by the bank |
CERTIFICATE |
Yes | Path to .p12 (PKCS#12) or .pem client certificate |
CERTIFICATE_PASSWORD |
No | Password for the certificate file |
PRIVATE_KEY |
No | Path to private key .pem (if using PEM format) |
PRIVATE_KEY_PASSWORD |
No | Password for the private key |
CA_CERTIFICATE |
No | Path to bank's CA certificate (PSroot.pem) |
SUCCESS_URL |
Yes | URL to redirect client after successful payment |
ERROR_URL |
Yes | URL to redirect client after failed payment |
Optional settings:
PASHA_BANK_MERCHANT_HANDLER=https://ecomm.pashabank.az:18443/ecomm2/MerchantHandler PASHA_BANK_CLIENT_HANDLER=https://ecomm.pashabank.az:8463/ecomm2/ClientHandler PASHA_BANK_CURRENCY=944 # ISO-4217 numeric code (944=AZN, 840=USD, 978=EUR) PASHA_BANK_LANGUAGE=az # Language for card entry page (az, en, ru) PASHA_BANK_TIMEOUT=30 # HTTP timeout in seconds PASHA_BANK_SSL_VERIFY=true # SSL certificate verification PASHA_BANK_LOG_CHANNEL=stack # Log channel PASHA_BANK_LOG_LEVEL=info # Log level
SSL Certificate Setup
Pasha Bank uses mutual TLS (mTLS) with client certificates. The bank provides a ZIP archive containing:
certificate.<merchant_id>.pem— Merchant certificate (PEM)imakstore.ecpw.<merchant_id>.p12— PKCS#12 keystorePSroot.pem— Bank's CA certificate
You can use either the .p12 file directly or convert to PEM:
# Extract certificate from P12 openssl pkcs12 -in keystore.p12 -out merchant.cert.pem -clcerts -nokeys # Extract private key from P12 openssl pkcs12 -in keystore.p12 -out merchant.key.pem -nocerts -nodes
Usage
SMS Payment (Single-Step)
The most common payment flow. Customer enters card data on the bank's page.
use Sarkhanrasimoghlu\PashaBank\Contracts\PashaBankServiceInterface; use Sarkhanrasimoghlu\PashaBank\DataTransferObjects\PaymentRequest; use Sarkhanrasimoghlu\PashaBank\Enums\Currency; use Sarkhanrasimoghlu\PashaBank\Enums\Language; $service = app(PashaBankServiceInterface::class); $request = new PaymentRequest( amount: 49.99, currency: Currency::AZN, clientIp: request()->ip(), orderId: 'ORDER-12345', description: 'Premium subscription', language: Language::AZ, ); $response = $service->createPayment($request); // Redirect customer to bank's card entry page return redirect($response->redirectUrl); // $response->transactionId — Base64-encoded, 28 chars (save this!) // $response->redirectUrl — bank's ClientHandler URL with trans_id // $response->rawResponse — full API response
DMS Payment (Two-Step: Pre-Auth + Capture)
First authorize (block the amount), then capture later.
use Sarkhanrasimoghlu\PashaBank\DataTransferObjects\DmsCaptureRequest; // Step 1: Pre-authorize (block amount on customer's card) $response = $service->createDmsAuth($request); return redirect($response->redirectUrl); // Step 2: After customer returns and result is OK, capture the amount $capture = $service->executeDmsCapture(new DmsCaptureRequest( transactionId: $transactionId, amount: 49.99, currency: Currency::AZN, clientIp: request()->ip(), )); if ($capture->isSuccessful()) { // Amount captured successfully // $capture->rrn — Retrieval Reference Number // $capture->approvalCode — 6-char approval code }
Get Transaction Result (CRITICAL)
After the bank redirects the customer back, you MUST call getTransactionResult() within 3 minutes. If not called, the bank automatically reverses the transaction.
The package handles this automatically via the built-in return route (POST /pasha-bank/return), but you can also call it manually:
$result = $service->getTransactionResult($transactionId, request()->ip()); $result->isSuccessful(); // true if RESULT=OK and RESULT_CODE=000 $result->status; // TransactionStatus::Succeeded, Failed, etc. $result->result; // Result::OK, Failed, Pending, AutoReversed, etc. $result->resultCode; // ResultCode::Approved (000), DeclineInsufficientFunds (116), etc. $result->threeDSecure; // ThreeDSecureStatus::Authenticated, Declined, etc. $result->rrn; // "123456789012" — Retrieval Reference Number $result->approvalCode; // "123456" $result->cardNumber; // "4***********9999" (masked) $result->recurringPaymentId; // For recurring payments only $result->rawResponse; // Full response array
Reversal
Full or partial reversal of a transaction.
use Sarkhanrasimoghlu\PashaBank\DataTransferObjects\ReversalRequest; // Full reversal $response = $service->reversal(new ReversalRequest( transactionId: $transactionId, )); // Partial reversal $response = $service->reversal(new ReversalRequest( transactionId: $transactionId, amount: 15.00, )); // Suspected fraud (full reversal only) $response = $service->reversal(new ReversalRequest( transactionId: $transactionId, suspectedFraud: true, )); $response->isSuccessful(); // true if RESULT=OK, RESULT_CODE=400
Refund
Full or partial refund of a completed transaction.
use Sarkhanrasimoghlu\PashaBank\DataTransferObjects\RefundRequest; // Full refund $response = $service->refund(new RefundRequest( transactionId: $transactionId, )); // Partial refund $response = $service->refund(new RefundRequest( transactionId: $transactionId, amount: 10.00, )); $response->isSuccessful(); // true if RESULT=OK $response->refundTransactionId; // Refund transaction ID (can be used for reversal of refund)
End of Business Day
Must be called daily to close the business day and reconcile transactions.
$response = $service->endOfBusinessDay(); $response->isSuccessful(); // true if RESULT=OK $response->resultCode; // ReconciledInBalance (500) or ReconciledOutOfBalance (501) $response->debitTransactions; // Number of debit transactions $response->debitSum; // Sum of debit transactions $response->creditTransactions; // Number of credit transactions $response->creditSum; // Sum of credit transactions
Schedule it in your routes/console.php:
use Illuminate\Support\Facades\Schedule; use Sarkhanrasimoghlu\PashaBank\Contracts\PashaBankServiceInterface; Schedule::call(function () { app(PashaBankServiceInterface::class)->endOfBusinessDay(); })->dailyAt('23:59');
Payment Flow
SMS Flow (Single-Step)
1. Merchant → POST command=v → Bank API → TRANSACTION_ID
2. Merchant → Redirect customer → Bank's card entry page
3. Customer enters card data, completes 3D-Secure
4. Bank → Redirects customer → POST /pasha-bank/return (with trans_id)
5. Package → POST command=c → Bank API → RESULT (auto-handled)
6. Package → Redirects customer → success_url or error_url
DMS Flow (Two-Step)
1. Merchant → POST command=a → Bank API → TRANSACTION_ID (pre-auth)
2. Merchant → Redirect customer → Bank's card entry page
3. Customer enters card data, completes 3D-Secure
4. Bank → Redirects customer back → Package calls command=c
5. Later: Merchant → POST command=t → Bank API → Capture amount
Important: If command=c is not called within 3 minutes after the customer returns, the bank automatically reverses the transaction.
Events
The package dispatches Laravel events:
use Sarkhanrasimoghlu\PashaBank\Events\PaymentCreated; use Sarkhanrasimoghlu\PashaBank\Events\PaymentCompleted; use Sarkhanrasimoghlu\PashaBank\Events\PaymentFailed; // In EventServiceProvider or listener registration: protected $listen = [ PaymentCreated::class => [ // Fired when createPayment() or createDmsAuth() succeeds // $event->transactionId, $event->orderId, $event->amount, $event->currency ], PaymentCompleted::class => [ // Fired when command=c returns RESULT=OK // $event->transactionId, $event->status, $event->cardNumber, $event->rrn ], PaymentFailed::class => [ // Fired when command=c returns RESULT != OK // $event->transactionId, $event->result, $event->resultCode ], ];
Example listener:
class HandlePaymentCompleted { public function handle(PaymentCompleted $event): void { $order = Order::where('payment_id', $event->transactionId)->first(); $order->markAsPaid(); } }
Authentication
The package uses mutual TLS (mTLS) with SSL client certificates over TLSv1.2. Authentication is handled automatically:
- PKCS#12 (
.p12) and PEM formats supported - TLSv1.2 enforced with secure cipher suites
- Certificate paths configured via
.env - No token management needed (unlike OAuth2)
API Commands
| Command | Letter | Service Method | Description |
|---|---|---|---|
| SMS Transaction | v |
createPayment() |
Single-step payment |
| DMS Authorization | a |
createDmsAuth() |
Pre-authorize (block amount) |
| DMS Capture | t |
executeDmsCapture() |
Capture pre-authorized amount |
| Transaction Result | c |
getTransactionResult() |
Get result (auto-handled on return) |
| Reversal | r |
reversal() |
Full or partial reversal |
| Refund | k |
refund() |
Full or partial refund |
| End of Business Day | b |
endOfBusinessDay() |
Daily settlement |
Transaction Statuses
| Enum Case | Value | Description |
|---|---|---|
Pending |
pending |
Transaction registered, awaiting customer |
Succeeded |
succeeded |
Payment completed (RESULT=OK) |
Failed |
failed |
Payment failed (RESULT=FAILED) |
Declined |
declined |
Payment declined by bank |
Reversed |
reversed |
Reversed by merchant |
AutoReversed |
autoreversed |
Auto-reversed (command=c not called in time) |
Refunded |
refunded |
Fully refunded |
Timeout |
timeout |
Transaction timed out |
Result Codes
| Code | Description |
|---|---|
000 |
Approved |
100 |
Decline (general) |
101 |
Decline, expired card |
102 |
Decline, suspected fraud |
110 |
Decline, invalid amount |
116 |
Decline, insufficient funds |
400 |
Reversal accepted |
500 |
Reconciled, in balance |
501 |
Reconciled, out of balance |
See ResultCode enum for the full list of 26 documented codes.
Error Handling
All exceptions extend PashaBankException:
use Sarkhanrasimoghlu\PashaBank\Exceptions\PashaBankException; use Sarkhanrasimoghlu\PashaBank\Exceptions\HttpException; use Sarkhanrasimoghlu\PashaBank\Exceptions\InvalidPaymentException; use Sarkhanrasimoghlu\PashaBank\Exceptions\InvalidConfigurationException; try { $response = $service->createPayment($request); } catch (HttpException $e) { // Connection failed, SSL error, server error $context = $e->getContext(); // ['url' => '...', 'ssl_error' => '...'] } catch (InvalidPaymentException $e) { // Invalid amount, missing transaction ID, missing client IP } catch (InvalidConfigurationException $e) { // Missing terminal_id, certificate path, etc. } catch (PashaBankException $e) { // Catch-all for any package exception }
Database
The package creates a pasha_bank_transactions table:
| Column | Type | Description |
|---|---|---|
id |
bigint | Primary key |
transaction_id |
string | Pasha Bank transaction ID (unique) |
order_id |
string | Your application's order ID (indexed) |
amount |
decimal(10,2) | Payment amount |
currency |
string(3) | ISO-4217 numeric currency code |
status |
string | Transaction status (indexed, default: pending) |
message_type |
string(3) | SMS or DMS |
card_number |
string | Masked card number (nullable) |
rrn |
string(12) | Retrieval Reference Number (nullable) |
approval_code |
string(6) | Approval code (nullable) |
result |
string | RESULT value from bank (nullable) |
result_code |
string(3) | RESULT_CODE from bank (nullable) |
redirect_url |
string(2048) | Bank's checkout URL (nullable) |
raw_response |
json | API response (nullable) |
paid_at |
timestamp | When payment succeeded (nullable) |
created_at |
timestamp | Record creation time |
updated_at |
timestamp | Last update time |
Currency Codes
| Code | Currency |
|---|---|
944 |
AZN (Azerbaijani Manat) |
840 |
USD (US Dollar) |
978 |
EUR (Euro) |
826 |
GBP (Pound Sterling) |
Local Development
For local testing without a real bank certificate, set PASHA_BANK_SSL_VERIFY=false in your .env. This disables TLS 1.2 enforcement and certificate requirements, allowing you to test against a local mock server over plain HTTP:
PASHA_BANK_MERCHANT_HANDLER=http://127.0.0.1:9001/ecomm2/MerchantHandler PASHA_BANK_CLIENT_HANDLER=http://127.0.0.1:9001/ecomm2/ClientHandler PASHA_BANK_SSL_VERIFY=false
Never set ssl_verify=false in production.
Testing
./vendor/bin/phpunit
The package includes 52 tests covering:
- Configuration validation
- DTO construction, validation, and amount conversion
- Transaction result parsing (success, failure, timeout, auto-reversal)
- Service layer (SMS, DMS, reversal, refund, end of day)
- Return controller (success redirect, error redirect, exceptions)
License
MIT