sudiptpa / omnipay-esewa
Framework-agnostic eSewa ePay v2 payment SDK for PHP.
Requires
- php: >=8.2 <8.6
Requires (Dev)
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^10.5 || ^11.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.1
- rector/rector: ^1.2
- symfony/http-client: ^6.4 || ^7.0 || ^8.0
Conflicts
- sudiptpa/esewa-php-sdk: *
README
Framework-agnostic, zero-dependency PHP SDK for eSewa ePay v2, with an optional Omnipay v3 bridge.
Public API
This package exposes two public namespaces:
Sujip\\EsewaOmnipay\\Esewa
Highlights
- checkout, callback, and transaction flows built around request and result models
- typed value objects for amount, transaction UUID, product code, and reference ID
toArray()andfromArray()where they are useful- callback verification with explicit states:
verified,invalid_signature,replayed - zero-dependency core with built-in
CurlTransport - configurable retry policy and clock handling
- replay protection backed by filesystem or PDO storage
- optional PSR-18 transport support
- optional Omnipay v3 bridge
- PHP
8.2to8.5
Installation
composer require sudiptpa/omnipay-esewa
Optional PSR-18 usage:
composer require symfony/http-client nyholm/psr7
Documentation
Quick Start
<?php declare(strict_types=1); use Sujip\Esewa\Esewa; use Sujip\Esewa\Domain\Checkout\CheckoutRequest; $client = Esewa::make( merchantCode: 'EPAYTEST', secretKey: $_ENV['ESEWA_SECRET_KEY'], environment: 'uat', ); $intent = $client->checkout()->createIntent(new CheckoutRequest( amount: '100', taxAmount: '0', serviceCharge: '0', deliveryCharge: '0', transactionUuid: 'TXN-1001', productCode: 'EPAYTEST', successUrl: 'https://merchant.example.com/esewa/success', failureUrl: 'https://merchant.example.com/esewa/failure', ));
The request constructor accepts strings for convenience. Internally, those values are normalized into value objects. If you want stricter typing in your own code, build the value objects directly:
use Sujip\Esewa\Domain\Checkout\CheckoutRequest; use Sujip\Esewa\ValueObject\Amount; use Sujip\Esewa\ValueObject\ProductCode; use Sujip\Esewa\ValueObject\TransactionUuid; $request = new CheckoutRequest( amount: Amount::fromString('100'), taxAmount: Amount::fromString('0'), serviceCharge: Amount::fromString('0'), deliveryCharge: Amount::fromString('0'), transactionUuid: TransactionUuid::fromString('TXN-1001'), productCode: ProductCode::fromString('EPAYTEST'), successUrl: 'https://merchant.example.com/esewa/success', failureUrl: 'https://merchant.example.com/esewa/failure', );
Working With Models
The core models can be converted to arrays. That is mainly useful when you are crossing controller boundaries, queueing work, or saving fixtures for tests.
$payload = $request->toArray(); $restored = CheckoutRequest::fromArray($payload);
The client stays small. Most integrations only need three modules:
$client->checkout()$client->callbacks()$client->transactions()
Callback Verification
use Sujip\Esewa\Domain\Verification\CallbackPayload; use Sujip\Esewa\Domain\Verification\VerificationExpectation; $payload = CallbackPayload::fromArray([ 'data' => $_GET['data'] ?? '', 'signature' => $_GET['signature'] ?? '', ]); $result = $client->callbacks()->verifyCallback( $payload, new VerificationExpectation( totalAmount: '100.00', transactionUuid: 'TXN-1001', productCode: 'EPAYTEST', ) ); if ($result->state->value === 'replayed') { http_response_code(409); exit('Replay detected'); } if (!$result->isSuccessful()) { http_response_code(400); exit('Invalid callback'); }
Do not treat the success redirect alone as proof of payment. Verify the callback on your backend and keep a status check as a fallback when something looks off.
Transaction Status
use Sujip\Esewa\Domain\Transaction\TransactionStatusRequest; $status = $client->transactions()->fetchStatus(new TransactionStatusRequest( transactionUuid: 'TXN-1001', totalAmount: '100.00', productCode: 'EPAYTEST', )); if ($status->isSuccessful()) { // mark paid }
Production Notes
For live traffic, turn on replay protection and use persistent storage:
use Sujip\Esewa\Config\ClientOptions; use Sujip\Esewa\Esewa; use Sujip\Esewa\Infrastructure\Idempotency\FilesystemIdempotencyStore; $client = Esewa::make( merchantCode: 'EPAYTEST', secretKey: $_ENV['ESEWA_SECRET_KEY'], environment: 'uat', options: new ClientOptions( preventCallbackReplay: true, idempotencyStore: new FilesystemIdempotencyStore(__DIR__ . '/storage/esewa-idempotency'), ), );
Retry behavior is also configurable:
use Sujip\Esewa\Config\ClientOptions; use Sujip\Esewa\Support\FixedDelayRetryPolicy; $options = new ClientOptions( retryPolicy: new FixedDelayRetryPolicy( maxRetries: 3, delayUs: 250000, ), );
Omnipay Bridge
use Omnipay\Esewa\SecureGateway; $gateway = new SecureGateway(); $gateway->setMerchantCode('EPAYTEST'); $gateway->setSecretKey($_ENV['ESEWA_SECRET_KEY']); $gateway->setProductCode('EPAYTEST'); $gateway->setTestMode(true); $gateway->setReturnUrl('https://merchant.example.com/esewa/success'); $gateway->setFailureUrl('https://merchant.example.com/esewa/failure');
Supported bridge flows:
purchase()completePurchase()verifyPayment()
Development
composer test
composer stan
composer rector:check