diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5b76aba..59f91c1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,8 +19,15 @@ jobs:
strategy:
fail-fast: false
matrix:
- php: ['8.1', '8.2', '8.3', '8.4', '8.5']
- dependency-version: ['prefer-stable', 'prefer-lowest']
+ include:
+ - php: '8.2'
+ dependency-version: 'prefer-lowest'
+ - php: '8.2'
+ dependency-version: 'prefer-stable'
+ - php: '8.3'
+ dependency-version: 'prefer-stable'
+ - php: '8.4'
+ dependency-version: 'prefer-stable'
steps:
- name: Checkout code
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 6fd5449..0f5a4f2 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -2,27 +2,50 @@
## Overview
-`Sujip\Esewa` is a framework-agnostic eSewa ePay v2 SDK focused on checkout payload generation, callback verification, and transaction status validation.
-The package follows a domain-oriented structure with clear service boundaries and pluggable transport/idempotency contracts.
+The package is split into a framework-agnostic core SDK and an Omnipay bridge that sits on top of it.
+
+The public namespaces are:
+
+- `Sujip\\Esewa` for the framework-agnostic client
+- `Omnipay\\Esewa` for the Omnipay bridge
+
+The design is intentionally conservative:
+
+- payment rules live in one place
+- transport and storage stay replaceable
+- callback verification stays deterministic
+- the core stays zero-dependency by default
+
+## Design choices
+
+1. Core payment flows use dedicated models instead of loose arrays.
+2. Money and identifiers are normalized early, so validation is not scattered.
+3. The default runtime works with built-in pieces.
+4. Callback verification and replay handling are part of the package, not left to application code.
+5. Transport, clock, retry policy, and replay storage can be replaced without rewriting the domain layer.
## Project Map
```text
src/
- EsewaPayment.php
+ Esewa.php Static entry point
+ Client/
+ EsewaClient.php Root client
+ CheckoutService.php Checkout workflow
+ CallbackService.php Callback workflow
+ TransactionService.php Status workflow
Config/
- GatewayConfig.php Gateway credentials + runtime options
- EndpointResolver.php UAT/production endpoint resolution
+ GatewayConfig.php Credentials and endpoint settings
+ EndpointResolver.php UAT/production endpoint resolution
Environment.php
- ClientOptions.php
- Client/
- EsewaClient.php Root client exposing service modules
- CheckoutService.php Checkout/form payload workflow
- CallbackService.php Callback decode + verification workflow
- TransactionService.php Status check workflow
- Service/
- SignatureService.php HMAC signature generation/verification
- CallbackVerifier.php Anti-fraud + callback validation rules
+ ClientOptions.php Runtime options, retry policy, clock, replay config
+ Contracts/
+ Arrayable.php
+ Hydratable.php
+ ClockInterface.php
+ RetryPolicyInterface.php
+ TransportInterface.php
+ IdempotencyStoreInterface.php
Domain/
Checkout/
CheckoutRequest.php
@@ -31,79 +54,201 @@ src/
Verification/
CallbackPayload.php
CallbackData.php
- CallbackVerification.php
VerificationExpectation.php
+ CallbackVerification.php
+ VerificationState.php
Transaction/
TransactionStatusRequest.php
TransactionStatusPayload.php
TransactionStatus.php
PaymentStatus.php
- Contracts/
- TransportInterface.php
- IdempotencyStoreInterface.php
+ ValueObject/
+ Amount.php
+ TransactionUuid.php
+ ProductCode.php
+ ReferenceId.php
+ Service/
+ SignatureService.php
+ CallbackVerifier.php
Infrastructure/
- Transport/Psr18Transport.php
- Idempotency/InMemoryIdempotencyStore.php
- Idempotency/NullIdempotencyStore.php
- Exception/
- EsewaException.php
- SignatureException.php
- InvalidPayloadException.php
- FraudValidationException.php
- ApiErrorException.php
- TransportException.php
-tests/
- Unit/*
- Fakes/FakeTransport.php
- Fakes/FlakyTransport.php
- Fakes/ArrayLogger.php
+ Transport/
+ CurlTransport.php
+ Psr18Transport.php
+ Idempotency/
+ InMemoryIdempotencyStore.php
+ NullIdempotencyStore.php
+ FilesystemIdempotencyStore.php
+ PdoIdempotencyStore.php
+ Support/
+ SystemClock.php
+ FixedDelayRetryPolicy.php
+ Omnipay/
+ SecureGateway.php
+ Message/*
```
-## Runtime Flows
+## Layer breakdown
+
+### 1. Entry Layer
+
+`Esewa` is the convenience bootstrap.
+
+Responsibilities:
+
+- build `GatewayConfig`
+- choose the transport
+- construct `EsewaClient`
+
+### 2. Client Layer
+
+`EsewaClient` exposes the three functional modules:
+
+- `checkout()`
+- `callbacks()`
+- `transactions()`
+
+The client is intentionally thin. It wires the pieces together and leaves the actual rules to the domain and service layers.
+
+### 3. Domain Layer
+
+The domain layer is where most of the package behavior lives.
+
+Responsibilities:
+
+- validate request shape
+- normalize primitives into value objects
+- convert to and from arrays
+- expose explicit result types
+
+Examples:
+
+- `CheckoutRequest` models a full checkout intent input
+- `VerificationExpectation` models anti-fraud comparison context
+- `TransactionStatusRequest` models reconciliation queries
+- `CallbackVerification` models callback outcome with explicit `VerificationState`
+
+### 4. Value Object Layer
+
+Value objects exist to stop the usual payment bugs caused by passing strings around everywhere.
+
+Current value objects:
+
+- `Amount`
+- `TransactionUuid`
+- `ProductCode`
+- `ReferenceId`
+
+This keeps formatting rules and basic validation in one place.
+
+### 5. Service Layer
+
+The service layer holds logic that should not be duplicated across controllers or adapters.
+
+- `SignatureService` generates and verifies signatures
+- `CallbackVerifier` enforces signature validation, expectation matching, and replay protection
+
+### 6. Infrastructure Layer
+
+Infrastructure concerns stay behind contracts.
+
+Transport:
+
+- `CurlTransport` for the zero-dependency runtime path
+- `Psr18Transport` for optional PSR-18 integration
+
+Replay protection stores:
+
+- `NullIdempotencyStore`
+- `InMemoryIdempotencyStore`
+- `FilesystemIdempotencyStore`
+- `PdoIdempotencyStore`
+
+### 7. Support Layer
+
+Support abstractions keep time and retry behavior deterministic.
+
+- `SystemClock`
+- `FixedDelayRetryPolicy`
+
+These are deliberately small. Their job is to keep time-dependent and retry-dependent logic testable.
+
+## Runtime flows
+
+### Checkout Flow
+
+1. Build `CheckoutRequest`
+2. `CheckoutService` computes total amount and signature
+3. `CheckoutPayload` is created as a typed form payload
+4. `CheckoutIntent` exposes action URL and form fields
+
+### Callback Verification Flow
+
+1. Build `CallbackPayload`
+2. Decode into `CallbackData`
+3. Verify signature with `SignatureService`
+4. Validate expected values with `VerificationExpectation`
+5. Check replay protection through `IdempotencyStoreInterface`
+6. Return `CallbackVerification` with an explicit `VerificationState`
+
+### Transaction Status Flow
+
+1. Build `TransactionStatusRequest`
+2. `TransactionService` performs the status request through `TransportInterface`
+3. Retry behavior is delegated to `RetryPolicyInterface`
+4. API payload is mapped to `TransactionStatusPayload`
+5. Domain result is returned as `TransactionStatus`
+
+## Model pattern
+
+The model pattern here is intentionally simple:
+
+- request models support `fromArray()` and `toArray()`
+- result models expose typed fields and `toArray()`
+- value objects encapsulate primitive validation
+
+That keeps the SDK easy to use from plain PHP, while still making it easier to cross framework boundaries cleanly.
+
+## Extensibility points
+
+Transport:
+
+- implement `TransportInterface`
+
+Replay protection:
-### 1) Checkout Flow
+- implement `IdempotencyStoreInterface`
-1. Build client via `EsewaPayment` using `GatewayConfig`.
-2. `CheckoutService` validates request + options.
-3. `SignatureService` signs required fields.
-4. Service returns `CheckoutPayload` for HTML form redirect/post.
+Time:
-### 2) Callback Verification Flow
+- implement `ClockInterface`
-1. Callback payload is decoded into `CallbackPayload`/`CallbackData`.
-2. `CallbackVerifier` validates signature and anti-fraud expectations.
-3. `CallbackService` returns structured `CallbackVerification`.
-4. Optional idempotency guard/store prevents duplicate side effects.
+Retry behavior:
-### 3) Transaction Status Flow
+- implement `RetryPolicyInterface`
-1. `TransactionService` sends status request using `TransportInterface`.
-2. Payload is mapped into `TransactionStatusPayload` and `TransactionStatus` enums.
-3. Caller applies business policy based on typed status.
+That gives applications room to swap in their own transport or persistence choices without adding mandatory packages to the core.
-## Security Model
+## Omnipay Bridge
-- Signature validation is centralized in `SignatureService`.
-- Callback validation enforces expectation checks (amount/product/uuid/reference).
-- Fraud mismatches raise `FraudValidationException`.
-- Invalid callback payloads raise `InvalidPayloadException`.
-- Transport/API failures are isolated in typed exceptions.
+`Omnipay\\Esewa` is kept separate on purpose.
-## Extension Points
+Bridge responsibilities:
-- Implement `TransportInterface` to integrate any HTTP client.
-- Implement `IdempotencyStoreInterface` for Redis/DB-backed duplicate protection.
-- Extend domain models and mapping rules without coupling to framework code.
+- adapt Omnipay request/response expectations
+- reuse the same core domain and service logic
+- avoid maintaining two different sets of payment rules
-## Testing Strategy
+## Testing
-- Unit tests cover checkout, callback, transaction, endpoint resolution, and production hardening paths.
-- Fake transports provide deterministic network behavior.
-- Flaky transport tests validate failure handling.
+The current test suite covers:
-## Contributor Notes
+- domain validation
+- checkout payload generation
+- callback signature verification
+- replay protection behavior
+- retry policy behavior
+- persistent idempotency stores
+- Omnipay bridge behavior
+- static analysis across the full source tree
-- Keep core package framework-agnostic.
-- Preserve public API stability; prefer additive changes.
-- Add tests and README updates for behavior changes.
-- Keep configuration explicit and secure by default.
+The point of that coverage is simple: payment verification and reconciliation code should stay boring, predictable, and hard to regress.
diff --git a/README.md b/README.md
index 0719cf6..9f9f691 100644
--- a/README.md
+++ b/README.md
@@ -1,143 +1,67 @@
-# EsewaPayment PHP SDK
+# eSewa PHP SDK
-Framework-agnostic eSewa ePay v2 SDK for modern PHP applications.
+Framework-agnostic, zero-dependency PHP SDK for eSewa ePay v2, with an optional Omnipay v3 bridge.
-[](https://github.com/sudiptpa/esewa/actions/workflows/ci.yml)
-[](https://github.com/sudiptpa/esewa/releases)
-[](https://github.com/sudiptpa/esewa/releases)
-[](https://www.php.net/)
-[](https://packagist.org/packages/sudiptpa/esewa-payment)
-[](LICENSE)
+[](https://github.com/sudiptpa/esewa-sdk-php/actions/workflows/ci.yml)
+[](https://github.com/sudiptpa/esewa-sdk-php/releases)
+[](https://github.com/sudiptpa/esewa-sdk-php/releases)
+[](https://www.php.net/)
+[](https://packagist.org/packages/sudiptpa/omnipay-esewa)
+[](LICENSE)
+
+## Public API
+
+This package exposes two public namespaces:
+
+- `Sujip\\Esewa`
+- `Omnipay\\Esewa`
## Highlights
-- ePay v2 checkout intent generation (`/v2/form`)
-- HMAC-SHA256 + base64 signature generation and verification
-- Callback verification with anti-fraud field consistency checks
-- Status check API support with typed status mapping
-- Model-first request/payload/result objects
-- PSR-18 transport integration
-- PHP `8.1` to `8.5` support
-- PHPUnit + PHPStan + CI matrix
-
-## Table of Contents
-
-- [Installation](#installation)
-- [Quick Start](#quick-start)
-- [Core API Shape](#core-api-shape)
-- [Checkout Flow](#checkout-flow)
-- [Callback Verification Flow](#callback-verification-flow)
-- [Transaction Status Flow](#transaction-status-flow)
-- [Configuration Patterns](#configuration-patterns)
-- [Production Hardening](#production-hardening)
-- [Laravel Integration (Secure)](#laravel-integration-secure)
-- [Custom Transport and Testing](#custom-transport-and-testing)
-- [Error Handling](#error-handling)
-- [Development](#development)
+- checkout, callback, and transaction flows built around request and result models
+- typed value objects for amount, transaction UUID, product code, and reference ID
+- `toArray()` and `fromArray()` 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.2` to `8.5`
## Installation
```bash
-composer require sudiptpa/esewa-payment
+composer require sudiptpa/omnipay-esewa
```
-For PSR-18 usage examples:
+Optional PSR-18 usage:
```bash
composer require symfony/http-client nyholm/psr7
```
-## Quick Start
-
-```php
-checkout()`
-- `$client->callbacks()`
-- `$client->transactions()`
-
-Primary model objects:
-
-- `CheckoutRequest`
-- `CheckoutIntent`
-- `CallbackPayload`
-- `VerificationExpectation`
-- `CallbackVerification`
-- `TransactionStatusRequest`
-- `TransactionStatus`
-
-Static convenience entry point:
-
-```php
-use EsewaPayment\EsewaPayment;
-
-$client = EsewaPayment::make(
- merchantCode: 'EPAYTEST',
- secretKey: 'secret',
- transport: $transport,
-);
-```
-
-## Checkout Flow
-
-### 1) Build a checkout intent
-
-```php
-use EsewaPayment\Domain\Checkout\CheckoutRequest;
$intent = $client->checkout()->createIntent(new CheckoutRequest(
amount: '100',
@@ -151,72 +75,78 @@ $intent = $client->checkout()->createIntent(new CheckoutRequest(
));
```
-### 2) Render form fields in plain PHP
+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:
```php
-$form = $intent->form();
+use Sujip\Esewa\Domain\Checkout\CheckoutRequest;
+use Sujip\Esewa\ValueObject\Amount;
+use Sujip\Esewa\ValueObject\ProductCode;
+use Sujip\Esewa\ValueObject\TransactionUuid;
-echo '
';
+$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',
+);
```
-### 3) Get fields directly as array
+## 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.
```php
-$fields = $intent->fields(); // array
+$payload = $request->toArray();
+$restored = CheckoutRequest::fromArray($payload);
```
-## Callback Verification Flow
+The client stays small. Most integrations only need three modules:
-Never trust redirect success alone. Always verify callback payload and signature.
+- `$client->checkout()`
+- `$client->callbacks()`
+- `$client->transactions()`
-### 1) Build payload from callback request
+## Callback Verification
```php
-use EsewaPayment\Domain\Verification\CallbackPayload;
+use Sujip\Esewa\Domain\Verification\CallbackPayload;
+use Sujip\Esewa\Domain\Verification\VerificationExpectation;
$payload = CallbackPayload::fromArray([
'data' => $_GET['data'] ?? '',
'signature' => $_GET['signature'] ?? '',
]);
-```
-### 2) Verify with anti-fraud expectation context
-
-```php
-use EsewaPayment\Domain\Verification\VerificationExpectation;
-
-$verification = $client->callbacks()->verifyCallback(
+$result = $client->callbacks()->verifyCallback(
$payload,
new VerificationExpectation(
totalAmount: '100.00',
transactionUuid: 'TXN-1001',
productCode: 'EPAYTEST',
- referenceId: null, // optional
)
);
-if (!$verification->valid || !$verification->isSuccessful()) {
- // reject
+if ($result->state->value === 'replayed') {
+ http_response_code(409);
+ exit('Replay detected');
}
-```
-### 3) Verify without context (signature only)
-
-```php
-$verification = $client->callbacks()->verifyCallback($payload);
+if (!$result->isSuccessful()) {
+ http_response_code(400);
+ exit('Invalid callback');
+}
```
-## Transaction Status Flow
+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
```php
-use EsewaPayment\Domain\Transaction\TransactionStatusRequest;
+use Sujip\Esewa\Domain\Transaction\TransactionStatusRequest;
$status = $client->transactions()->fetchStatus(new TransactionStatusRequest(
transactionUuid: 'TXN-1001',
@@ -225,308 +155,63 @@ $status = $client->transactions()->fetchStatus(new TransactionStatusRequest(
));
if ($status->isSuccessful()) {
- // COMPLETE
+ // mark paid
}
-
-echo $status->status->value; // PENDING|COMPLETE|FULL_REFUND|PARTIAL_REFUND|AMBIGUOUS|NOT_FOUND|CANCELED|UNKNOWN
```
-## Configuration Patterns
+## Production Notes
-### Environment aliases
-
-- UAT: `uat`, `test`, `sandbox`
-- Production: `production`, `prod`, `live`
-
-### Endpoint overrides
-
-Useful if eSewa documentation/endpoints differ by account region or rollout:
+For live traffic, turn on replay protection and use persistent storage:
```php
-$config = GatewayConfig::make(
+use Sujip\Esewa\Config\ClientOptions;
+use Sujip\Esewa\Esewa;
+use Sujip\Esewa\Infrastructure\Idempotency\FilesystemIdempotencyStore;
+
+$client = Esewa::make(
merchantCode: 'EPAYTEST',
- secretKey: 'secret',
+ secretKey: $_ENV['ESEWA_SECRET_KEY'],
environment: 'uat',
- checkoutFormUrl: 'https://custom-esewa.example/api/epay/main/v2/form',
- statusCheckUrl: 'https://custom-esewa.example/api/epay/transaction/status/',
-);
-```
-
-## Production Hardening
-
-### Retry policy for status checks
-
-`fetchStatus()` retries `TransportException` failures based on `ClientOptions`:
-
-```php
-new ClientOptions(
- maxStatusRetries: 2, // total additional retry attempts
- statusRetryDelayMs: 150,
+ options: new ClientOptions(
+ preventCallbackReplay: true,
+ idempotencyStore: new FilesystemIdempotencyStore(__DIR__ . '/storage/esewa-idempotency'),
+ ),
);
```
-### Callback replay protection (idempotency)
-
-Enable replay protection with an idempotency store:
+Retry behavior is also configurable:
```php
-use EsewaPayment\Config\ClientOptions;
-use EsewaPayment\Infrastructure\Idempotency\InMemoryIdempotencyStore;
+use Sujip\Esewa\Config\ClientOptions;
+use Sujip\Esewa\Support\FixedDelayRetryPolicy;
$options = new ClientOptions(
- preventCallbackReplay: true,
- idempotencyStore: new InMemoryIdempotencyStore(),
+ retryPolicy: new FixedDelayRetryPolicy(
+ maxRetries: 3,
+ delayUs: 250000,
+ ),
);
```
-For production, implement `IdempotencyStoreInterface` with shared storage (Redis, DB, etc.) instead of in-memory storage.
-
-### Logging hooks
-
-Provide any PSR-3 logger in `ClientOptions`:
+## Omnipay Bridge
```php
-use Psr\Log\LoggerInterface;
+use Omnipay\Esewa\SecureGateway;
-$options = new ClientOptions(logger: $logger); // $logger is LoggerInterface
+$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');
```
-Emitted event keys (via log context):
-
-- `esewa.status.started`
-- `esewa.status.retry`
-- `esewa.status.completed`
-- `esewa.status.failed`
-- `esewa.callback.invalid_signature`
-- `esewa.callback.replay_detected`
-- `esewa.callback.verified`
-
-## Laravel Integration (Secure)
-
-### Supported Laravel Versions
-
-This package is framework-agnostic and supports PHP `8.1` to `8.5`.
-
-Laravel support is therefore:
-
-- Laravel `10.x`, `11.x`, `12.x` when your app runtime is PHP `8.1` to `8.5`
-- Other Laravel versions may work if they run on supported PHP and PSR dependencies, but are not the primary target matrix
-
-### 1) Service container binding (single client, production options)
-
-Create a provider (for example `app/Providers/EsewaServiceProvider.php`):
-
-```php
-use EsewaPayment\Client\EsewaClient;
-use EsewaPayment\Config\ClientOptions;
-use EsewaPayment\Config\GatewayConfig;
-use EsewaPayment\Contracts\IdempotencyStoreInterface;
-use EsewaPayment\Infrastructure\Idempotency\InMemoryIdempotencyStore;
-use EsewaPayment\Infrastructure\Transport\Psr18Transport;
-use Nyholm\Psr7\Factory\Psr17Factory;
-use Symfony\Component\HttpClient\Psr18Client;
-
-$this->app->singleton(IdempotencyStoreInterface::class, function () {
- // Replace with Redis/DB-backed implementation for multi-server production.
- return new InMemoryIdempotencyStore();
-});
-
-$this->app->singleton(EsewaClient::class, function ($app) {
- return new EsewaClient(
- GatewayConfig::make(
- merchantCode: config('services.esewa.merchant_code'),
- secretKey: config('services.esewa.secret_key'),
- environment: config('services.esewa.environment', 'uat'),
- ),
- new Psr18Transport(new Psr18Client(), new Psr17Factory()),
- new ClientOptions(
- maxStatusRetries: 2,
- statusRetryDelayMs: 150,
- preventCallbackReplay: true,
- idempotencyStore: $app->make(IdempotencyStoreInterface::class),
- logger: $app->make(\Psr\Log\LoggerInterface::class),
- ),
- );
-});
-```
+Supported bridge flows:
-### 2) Route design (do not trust success redirect)
-
-Use separate callback verification endpoint and finalize order only after verification:
-
-```php
-use App\Http\Controllers\EsewaCallbackController;
-use App\Http\Controllers\EsewaCheckoutController;
-use Illuminate\Support\Facades\Route;
-
-Route::post('/payments/esewa/checkout', [EsewaCheckoutController::class, 'store'])
- ->name('payments.esewa.checkout');
-
-Route::get('/payments/esewa/success', [EsewaCheckoutController::class, 'success'])
- ->name('payments.esewa.success');
-
-Route::get('/payments/esewa/failure', [EsewaCheckoutController::class, 'failure'])
- ->name('payments.esewa.failure');
-
-Route::post('/payments/esewa/callback', [EsewaCallbackController::class, 'handle'])
- ->name('payments.esewa.callback');
-```
-
-### 3) Checkout controller (server-side source of truth)
-
-```php
-use EsewaPayment\Client\EsewaClient;
-use EsewaPayment\Domain\Checkout\CheckoutRequest;
-use Illuminate\Http\Request;
-
-final class EsewaCheckoutController
-{
- public function store(Request $request, EsewaClient $esewa)
- {
- $order = /* create order in DB and generate transaction UUID */;
-
- $intent = $esewa->checkout()->createIntent(new CheckoutRequest(
- amount: (string) $order->amount,
- taxAmount: '0',
- serviceCharge: '0',
- deliveryCharge: '0',
- transactionUuid: $order->transaction_uuid,
- productCode: config('services.esewa.merchant_code'),
- successUrl: route('payments.esewa.success'),
- failureUrl: route('payments.esewa.failure'),
- ));
-
- return view('payments.esewa.redirect', [
- 'action' => $intent->actionUrl,
- 'fields' => $intent->fields(),
- ]);
- }
-}
-```
-
-`resources/views/payments/esewa/redirect.blade.php`:
-
-```blade
-
-
-
-```
-
-### 4) Callback controller with strict verification
-
-```php
-use EsewaPayment\Client\EsewaClient;
-use EsewaPayment\Domain\Verification\CallbackPayload;
-use EsewaPayment\Domain\Verification\VerificationExpectation;
-use Illuminate\Http\Request;
-use Symfony\Component\HttpFoundation\Response;
-
-final class EsewaCallbackController
-{
- public function handle(Request $request, EsewaClient $esewa): Response
- {
- $payload = CallbackPayload::fromArray([
- 'data' => (string) $request->input('data', ''),
- 'signature' => (string) $request->input('signature', ''),
- ]);
-
- $order = /* lookup order using decoded transaction UUID */;
- $expectation = new VerificationExpectation(
- totalAmount: number_format((float) $order->payable_amount, 2, '.', ''),
- transactionUuid: $order->transaction_uuid,
- productCode: config('services.esewa.merchant_code'),
- referenceId: null,
- );
-
- $result = $esewa->callbacks()->verifyCallback($payload, $expectation);
-
- if (!$result->isSuccessful()) {
- return response('invalid', 400);
- }
-
- // Optional: double-check status endpoint before marking paid.
- // $status = $esewa->transactions()->fetchStatus(...);
-
- // Mark paid exactly once (idempotent DB update).
- // dispatch(new FulfillOrderJob($order->id));
-
- return response('ok', 200);
- }
-}
-```
-
-### 5) Security checklist (Laravel)
-
-- Keep `merchant_code` and `secret_key` only in `.env`
-- Never trust only `success` redirect for payment finalization
-- Verify callback signature and anti-fraud fields every time
-- Enforce idempotent order updates in DB and callback processing
-- Log verification failures and replay attempts
-- Queue downstream fulfillment after verified payment
-
-### Framework usage outside Laravel
-
-- Register `GatewayConfig` as a service parameter object
-- Inject `EsewaClient` into controllers/services
-- Use `checkout`, `callbacks`, and `transactions` modules in your application service layer
-
-## Custom Transport and Testing
-
-You can use any custom transport implementing `TransportInterface`.
-
-```php
-use EsewaPayment\Contracts\TransportInterface;
-
-final class FakeTransport implements TransportInterface
-{
- public function get(string $url, array $query = [], array $headers = []): array
- {
- return ['status' => 'COMPLETE', 'ref_id' => 'REF-123'];
- }
-}
-```
-
-Inject it:
-
-```php
-$client = new EsewaClient($config, new FakeTransport());
-```
-
-## Error Handling
-
-Key exceptions:
-
-- `InvalidPayloadException`
-- `FraudValidationException`
-- `TransportException`
-- `ApiErrorException`
-- Base: `EsewaException`
-
-Typical handling:
-
-```php
-use EsewaPayment\Exception\ApiErrorException;
-use EsewaPayment\Exception\EsewaException;
-use EsewaPayment\Exception\FraudValidationException;
-use EsewaPayment\Exception\InvalidPayloadException;
-use EsewaPayment\Exception\TransportException;
-
-try {
- $verification = $client->callbacks()->verifyCallback($payload, $expectation);
-} catch (FraudValidationException|InvalidPayloadException $e) {
- // fail closed
-} catch (TransportException|ApiErrorException $e) {
- // retry/report policy
-} catch (EsewaException $e) {
- // domain fallback policy
-}
-```
+- `purchase()`
+- `completePurchase()`
+- `verifyPayment()`
## Development
@@ -535,20 +220,3 @@ composer test
composer stan
composer rector:check
```
-
-## Author
-
-- Sujip Thapa ()
-
-## Contributing
-
-Contributions are welcome.
-
-If you would like to contribute:
-
-1. Fork the repository and create a feature branch.
-2. Add or update tests for your change.
-3. Run quality checks locally (`composer test`, `composer stan`).
-4. Open a pull request with a clear description.
-
-Bug reports, security hardening ideas, docs improvements, and real-world integration examples are all appreciated.
diff --git a/composer.json b/composer.json
index 0fcf63e..c8f4152 100644
--- a/composer.json
+++ b/composer.json
@@ -1,5 +1,5 @@
{
- "name": "sudiptpa/esewa-payment",
+ "name": "sudiptpa/omnipay-esewa",
"type": "library",
"description": "Framework-agnostic eSewa ePay v2 payment SDK for PHP.",
"keywords": [
@@ -17,33 +17,29 @@
}
],
"require": {
- "php": ">=8.1 <8.6",
- "psr/http-client": "^1.0",
- "psr/http-factory": "^1.1",
- "psr/http-message": "^2.0",
- "psr/log": "^1.1 || ^2.0 || ^3.0"
+ "php": ">=8.2 <8.6"
},
"require-dev": {
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.1",
"phpunit/phpunit": "^10.5 || ^11.0",
"phpstan/phpstan": "^1.11",
"rector/rector": "^1.2",
"symfony/http-client": "^6.4 || ^7.0 || ^8.0",
"nyholm/psr7": "^1.8"
},
- "suggest": {
- "sudiptpa/omnipay-esewa": "Legacy package name. This SDK has moved to sudiptpa/esewa-payment."
- },
"conflict": {
- "sudiptpa/omnipay-esewa": "*"
+ "sudiptpa/esewa-php-sdk": "*"
},
"autoload": {
"psr-4": {
- "EsewaPayment\\": "src/"
+ "Sujip\\Esewa\\": "src/",
+ "Omnipay\\Esewa\\": "src/Omnipay/"
}
},
"autoload-dev": {
"psr-4": {
- "EsewaPayment\\Tests\\": "tests/"
+ "Sujip\\Esewa\\Tests\\": "tests/"
}
},
"extra": {
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..71542df
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,2583 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "5cdeb507e5d7f3d6227512700bc599d8",
+ "packages": [],
+ "packages-dev": [
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.13.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-01T08:46:24+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
+ },
+ "time": "2025-12-06T11:56:16+00:00"
+ },
+ {
+ "name": "nyholm/psr7",
+ "version": "1.8.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Nyholm/psr7.git",
+ "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3",
+ "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0"
+ },
+ "provide": {
+ "php-http/message-factory-implementation": "1.0",
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "http-interop/http-factory-tests": "^0.9",
+ "php-http/message-factory": "^1.0",
+ "php-http/psr7-integration-tests": "^1.0",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4",
+ "symfony/error-handler": "^4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Nyholm\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com"
+ },
+ {
+ "name": "Martijn van der Ven",
+ "email": "martijn@vanderven.se"
+ }
+ ],
+ "description": "A fast PHP7 implementation of PSR-7",
+ "homepage": "https://tnyholm.se",
+ "keywords": [
+ "psr-17",
+ "psr-7"
+ ],
+ "support": {
+ "issues": "https://github.com/Nyholm/psr7/issues",
+ "source": "https://github.com/Nyholm/psr7/tree/1.8.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Zegnat",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nyholm",
+ "type": "github"
+ }
+ ],
+ "time": "2024-09-09T07:06:30+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "1.12.33",
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1",
+ "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-28T20:30:03+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "11.0.12",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56",
+ "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^5.7.0",
+ "php": ">=8.2",
+ "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-text-template": "^4.0.1",
+ "sebastian/code-unit-reverse-lookup": "^4.0.1",
+ "sebastian/complexity": "^4.0.1",
+ "sebastian/environment": "^7.2.1",
+ "sebastian/lines-of-code": "^3.0.1",
+ "sebastian/version": "^5.0.2",
+ "theseer/tokenizer": "^1.3.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.5.46"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "11.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-24T07:01:01+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "5.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903",
+ "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-02-02T13:52:54+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "5.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2",
+ "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^11.0"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "security": "https://github.com/sebastianbergmann/php-invoker/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:07:44+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964",
+ "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "security": "https://github.com/sebastianbergmann/php-text-template/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:08:43+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "7.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3",
+ "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "security": "https://github.com/sebastianbergmann/php-timer/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:09:35+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "11.5.55",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00",
+ "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.13.4",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=8.2",
+ "phpunit/php-code-coverage": "^11.0.12",
+ "phpunit/php-file-iterator": "^5.1.1",
+ "phpunit/php-invoker": "^5.0.1",
+ "phpunit/php-text-template": "^4.0.1",
+ "phpunit/php-timer": "^7.0.1",
+ "sebastian/cli-parser": "^3.0.2",
+ "sebastian/code-unit": "^3.0.3",
+ "sebastian/comparator": "^6.3.3",
+ "sebastian/diff": "^6.0.2",
+ "sebastian/environment": "^7.2.1",
+ "sebastian/exporter": "^6.3.2",
+ "sebastian/global-state": "^7.0.2",
+ "sebastian/object-enumerator": "^6.0.1",
+ "sebastian/recursion-context": "^6.0.3",
+ "sebastian/type": "^5.1.3",
+ "sebastian/version": "^5.0.2",
+ "staabm/side-effects-detector": "^1.0.5"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "11.5-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsors.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-02-18T12:37:06+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
+ },
+ "time": "2024-09-11T13:17:53+00:00"
+ },
+ {
+ "name": "rector/rector",
+ "version": "1.2.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/rectorphp/rector.git",
+ "reference": "40f9cf38c05296bd32f444121336a521a293fa61"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/40f9cf38c05296bd32f444121336a521a293fa61",
+ "reference": "40f9cf38c05296bd32f444121336a521a293fa61",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2|^8.0",
+ "phpstan/phpstan": "^1.12.5"
+ },
+ "conflict": {
+ "rector/rector-doctrine": "*",
+ "rector/rector-downgrade-php": "*",
+ "rector/rector-phpunit": "*",
+ "rector/rector-symfony": "*"
+ },
+ "suggest": {
+ "ext-dom": "To manipulate phpunit.xml via the custom-rule command"
+ },
+ "bin": [
+ "bin/rector"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Instant Upgrade and Automated Refactoring of any PHP code",
+ "keywords": [
+ "automation",
+ "dev",
+ "migration",
+ "refactoring"
+ ],
+ "support": {
+ "issues": "https://github.com/rectorphp/rector/issues",
+ "source": "https://github.com/rectorphp/rector/tree/1.2.10"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/tomasvotruba",
+ "type": "github"
+ }
+ ],
+ "time": "2024-11-08T13:59:10+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180",
+ "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:41:36+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64",
+ "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "security": "https://github.com/sebastianbergmann/code-unit/security/policy",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-03-19T07:56:08+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "183a9b2632194febd219bb9246eee421dad8d45e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e",
+ "reference": "183a9b2632194febd219bb9246eee421dad8d45e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:45:54+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "6.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9",
+ "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-mbstring": "*",
+ "php": ">=8.2",
+ "sebastian/diff": "^6.0",
+ "sebastian/exporter": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.4"
+ },
+ "suggest": {
+ "ext-bcmath": "For comparing BcMath\\Number objects"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.3-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "security": "https://github.com/sebastianbergmann/comparator/security/policy",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-24T09:26:40+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "ee41d384ab1906c68852636b6de493846e13e5a0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0",
+ "reference": "ee41d384ab1906c68852636b6de493846e13e5a0",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "security": "https://github.com/sebastianbergmann/complexity/security/policy",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:49:50+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "6.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544",
+ "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "security": "https://github.com/sebastianbergmann/diff/security/policy",
+ "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:53:05+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "7.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4",
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "https://github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "security": "https://github.com/sebastianbergmann/environment/security/policy",
+ "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/environment",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-05-21T11:55:47+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "6.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "70a298763b40b213ec087c51c739efcaa90bcd74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74",
+ "reference": "70a298763b40b213ec087c51c739efcaa90bcd74",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=8.2",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.3-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "security": "https://github.com/sebastianbergmann/exporter/security/policy",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-09-24T06:12:51+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "7.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "3be331570a721f9a4b5917f4209773de17f747d7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7",
+ "reference": "3be331570a721f9a4b5917f4209773de17f747d7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "sebastian/object-reflector": "^4.0",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "https://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "security": "https://github.com/sebastianbergmann/global-state/security/policy",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:57:36+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a",
+ "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:58:38+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "6.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "f5b498e631a74204185071eb41f33f38d64608aa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa",
+ "reference": "f5b498e631a74204185071eb41f33f38d64608aa",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "sebastian/object-reflector": "^4.0",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:00:13+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9",
+ "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "security": "https://github.com/sebastianbergmann/object-reflector/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:01:32+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "6.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc",
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-13T04:42:22+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "5.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "security": "https://github.com/sebastianbergmann/type/security/policy",
+ "source": "https://github.com/sebastianbergmann/type/tree/5.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/type",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-09T06:55:48+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "5.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874",
+ "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "security": "https://github.com/sebastianbergmann/version/security/policy",
+ "source": "https://github.com/sebastianbergmann/version/tree/5.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-09T05:16:32+00:00"
+ },
+ {
+ "name": "staabm/side-effects-detector",
+ "version": "1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/staabm/side-effects-detector.git",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^1.12.6",
+ "phpunit/phpunit": "^9.6.21",
+ "symfony/var-dumper": "^5.4.43",
+ "tomasvotruba/type-coverage": "1.0.0",
+ "tomasvotruba/unused-public": "1.0.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "lib/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A static analysis tool to detect side effects in PHP code",
+ "keywords": [
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/staabm/side-effects-detector/issues",
+ "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/staabm",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-20T05:08:20+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:21:43+00:00"
+ },
+ {
+ "name": "symfony/http-client",
+ "version": "v8.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client.git",
+ "reference": "ade9bd433450382f0af154661fc8e72758b4de36"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/ade9bd433450382f0af154661fc8e72758b4de36",
+ "reference": "ade9bd433450382f0af154661fc8e72758b4de36",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "psr/log": "^1|^2|^3",
+ "symfony/http-client-contracts": "~3.4.4|^3.5.2",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "amphp/amp": "<3",
+ "php-http/discovery": "<1.15"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "*",
+ "php-http/client-implementation": "*",
+ "psr/http-client-implementation": "1.0",
+ "symfony/http-client-implementation": "3.0"
+ },
+ "require-dev": {
+ "amphp/http-client": "^5.3.2",
+ "amphp/http-tunnel": "^2.0",
+ "guzzlehttp/promises": "^1.4|^2.0",
+ "nyholm/psr7": "^1.0",
+ "php-http/httplug": "^1.0|^2.0",
+ "psr/http-client": "^1.0",
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/rate-limiter": "^7.4|^8.0",
+ "symfony/stopwatch": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "http"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client/tree/v8.0.7"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-06T13:17:40+00:00"
+ },
+ {
+ "name": "symfony/http-client-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client-contracts.git",
+ "reference": "75d7043853a42837e68111812f4d964b01e5101c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
+ "reference": "75d7043853a42837e68111812f4d964b01e5101c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to HTTP clients",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-04-29T11:18:49+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v3.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-07-15T11:30:57+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2025-11-17T20:03:58+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": true,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=8.2 <8.6"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "2.2.0"
+}
diff --git a/docs/migration-guide.md b/docs/migration-guide.md
new file mode 100644
index 0000000..6897bfd
--- /dev/null
+++ b/docs/migration-guide.md
@@ -0,0 +1,139 @@
+# Migration Guide
+
+## What changed
+
+The package now leans on request and result models instead of passing loose arrays and strings through every layer.
+
+The main differences are:
+
+- checkout, callback, and status flows are centered around domain models
+- request models support `toArray()` and `fromArray()`
+- core identifiers and money values are normalized into typed value objects
+- callback verification returns an explicit state
+- retry behavior can be configured as a policy
+- replay protection can use persistent zero-dependency storage
+
+## Public Namespaces
+
+- framework-agnostic client: `Sujip\\Esewa`
+- Omnipay bridge: `Omnipay\\Esewa`
+
+## Framework-Agnostic Client Migration
+
+### Earlier style
+
+Older integrations mostly passed strings around and treated request objects as thin containers.
+
+### Current style
+
+You can still pass strings to the constructors. The difference is that the models now normalize and validate those values more aggressively.
+
+```php
+use Sujip\Esewa\Domain\Checkout\CheckoutRequest;
+use Sujip\Esewa\Esewa;
+
+$client = Esewa::make(
+ merchantCode: 'EPAYTEST',
+ secretKey: 'secret',
+ environment: 'uat',
+);
+
+$request = 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',
+);
+```
+
+If you want stricter typing in your own code:
+
+```php
+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',
+);
+```
+
+## Model conversion
+
+Models support array conversion, which is useful at controller boundaries, in queued jobs, and in test fixtures.
+
+```php
+$payload = $request->toArray();
+$restored = CheckoutRequest::fromArray($payload);
+```
+
+The same pattern applies to:
+
+- `VerificationExpectation`
+- `TransactionStatusRequest`
+- `CallbackPayload`
+- `TransactionStatusPayload`
+
+## Callback verification
+
+Verification now returns a richer result object.
+
+```php
+$result = $client->callbacks()->verifyCallback($payload, $expectation);
+```
+
+Available states:
+
+- `verified`
+- `invalid_signature`
+- `replayed`
+
+That makes it easier to tell a bad signature from a replayed callback.
+
+## Retry and replay protection
+
+Retry behavior is no longer limited to a couple of scalar options. You can provide a policy object instead.
+
+```php
+use Sujip\Esewa\Config\ClientOptions;
+use Sujip\Esewa\Support\FixedDelayRetryPolicy;
+
+$options = new ClientOptions(
+ retryPolicy: new FixedDelayRetryPolicy(maxRetries: 3, delayUs: 250000),
+);
+```
+
+Replay protection can use persistent storage:
+
+- `FilesystemIdempotencyStore`
+- `PdoIdempotencyStore`
+
+## Omnipay migration
+
+The bridge remains under `Omnipay\\Esewa`.
+
+Supported methods:
+
+- `purchase()`
+- `completePurchase()`
+- `verifyPayment()`
+
+## Production checklist
+
+1. Keep `merchantCode` and `secretKey` in environment configuration.
+2. Verify callbacks on the backend before fulfillment.
+3. Compare verified values against stored order state.
+4. Use filesystem or PDO-backed replay protection in production.
+5. Reconcile uncertain states with transaction status checks.
+6. Treat redirect success as a user-facing signal, not as proof of payment.
diff --git a/docs/user-guide.md b/docs/user-guide.md
new file mode 100644
index 0000000..5cb442e
--- /dev/null
+++ b/docs/user-guide.md
@@ -0,0 +1,287 @@
+# User Guide
+
+There are two ways to use this package:
+
+- `Sujip\\Esewa` for the framework-agnostic client
+- `Omnipay\\Esewa` for the Omnipay bridge
+
+## 1. Framework-Agnostic Client
+
+### Create a client
+
+```php
+toArray();
+$restored = CheckoutRequest::fromArray($payload);
+```
+
+That is useful for:
+
+- controller boundaries
+- queue payloads
+- config-driven integrations
+- fixtures and tests
+
+### Create a checkout intent
+
+```php
+$intent = $client->checkout()->createIntent($request);
+$form = $intent->form();
+```
+
+### Verify callbacks
+
+```php
+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',
+ )
+);
+```
+
+### Handle verification states
+
+```php
+switch ($result->state->value) {
+ case 'verified':
+ // continue
+ break;
+
+ case 'replayed':
+ http_response_code(409);
+ exit('Replay detected');
+
+ case 'invalid_signature':
+ http_response_code(400);
+ exit('Invalid signature');
+}
+```
+
+### Enable replay protection
+
+For production, use persistent storage. The SDK ships with these store options:
+
+- `FilesystemIdempotencyStore`
+- `PdoIdempotencyStore`
+- `InMemoryIdempotencyStore` for tests
+
+Filesystem example:
+
+```php
+use Sujip\Esewa\Config\ClientOptions;
+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'),
+ )
+);
+```
+
+PDO example:
+
+```php
+use PDO;
+use Sujip\Esewa\Infrastructure\Idempotency\PdoIdempotencyStore;
+
+$pdo = new PDO('sqlite:' . __DIR__ . '/storage/esewa.sqlite');
+$store = new PdoIdempotencyStore($pdo);
+```
+
+### Reconcile transaction state
+
+```php
+use Sujip\Esewa\Domain\Transaction\TransactionStatusRequest;
+
+$status = $client->transactions()->fetchStatus(new TransactionStatusRequest(
+ transactionUuid: 'TXN-1001',
+ totalAmount: '100.00',
+ productCode: 'EPAYTEST',
+));
+```
+
+### Customize retries
+
+```php
+use Sujip\Esewa\Config\ClientOptions;
+use Sujip\Esewa\Support\FixedDelayRetryPolicy;
+
+$options = new ClientOptions(
+ retryPolicy: new FixedDelayRetryPolicy(
+ maxRetries: 3,
+ delayUs: 250000,
+ ),
+);
+```
+
+### Use a custom transport
+
+The default transport is `CurlTransport`, so the core stays zero-dependency at runtime. If your application already has a PSR-18 client, you can swap it in.
+
+```php
+use Nyholm\Psr7\Factory\Psr17Factory;
+use Sujip\Esewa\Infrastructure\Transport\Psr18Transport;
+use Symfony\Component\HttpClient\Psr18Client;
+
+$client = Esewa::make(
+ merchantCode: 'EPAYTEST',
+ secretKey: $_ENV['ESEWA_SECRET_KEY'],
+ transport: new Psr18Transport(new Psr18Client(), new Psr17Factory()),
+ environment: 'uat',
+);
+```
+
+## 2. Omnipay Bridge
+
+Use the bridge if the rest of your payment layer already follows Omnipay conventions.
+
+### Create the gateway
+
+```php
+use Omnipay\Esewa\SecureGateway;
+
+$gateway = new SecureGateway();
+$gateway->setMerchantCode('EPAYTEST');
+$gateway->setSecretKey($_ENV['ESEWA_SECRET_KEY']);
+$gateway->setProductCode('EPAYTEST');
+$gateway->setTaxAmount('0');
+$gateway->setServiceCharge('0');
+$gateway->setDeliveryCharge('0');
+$gateway->setTestMode(true);
+$gateway->setReturnUrl('https://merchant.example.com/esewa/success');
+$gateway->setFailureUrl('https://merchant.example.com/esewa/failure');
+```
+
+### Purchase flow
+
+```php
+$response = $gateway->purchase([
+ 'amount' => '100',
+ 'transactionId' => 'TXN-1001',
+])->send();
+
+if ($response->isRedirect()) {
+ $redirectUrl = $response->getRedirectUrl();
+ $fields = $response->getRedirectData();
+}
+```
+
+### Complete purchase
+
+```php
+$response = $gateway->completePurchase([
+ 'transactionUuid' => 'TXN-1001',
+ 'totalAmount' => '100.00',
+ 'referenceNumber' => 'REF-1001',
+])->send();
+
+if ($response->isSuccessful()) {
+ $reference = $response->getTransactionReference();
+}
+```
+
+### Verify payment
+
+```php
+$response = $gateway->verifyPayment([
+ 'amount' => '100.00',
+ 'transactionId' => 'TXN-1001',
+])->send();
+
+if ($response->isSuccessful()) {
+ $referenceId = $response->getReferenceId();
+}
+```
+
+## 3. Production Guidance
+
+1. Keep secrets outside source control.
+2. Verify callbacks on the server before fulfillment.
+3. Compare verified values with your stored order state.
+4. Use persistent replay protection in production.
+5. Treat redirect success as customer UX, not payment proof.
+6. Reconcile ambiguous states with transaction status checks.
+
+The common mistake here is trusting the browser redirect too early. eSewa can redirect a user back while your application still needs to verify the callback or reconcile the final state.
+
+## 4. Error Handling
+
+Core exceptions:
+
+- `Sujip\\Esewa\\Exception\\EsewaException`
+- `Sujip\\Esewa\\Exception\\InvalidPayloadException`
+- `Sujip\\Esewa\\Exception\\SignatureException`
+- `Sujip\\Esewa\\Exception\\FraudValidationException`
+- `Sujip\\Esewa\\Exception\\TransportException`
+- `Sujip\\Esewa\\Exception\\ApiErrorException`
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index e8ed43a..4d05e59 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,7 +1,7 @@
-
+
-
+
tests
diff --git a/src/Client/CallbackService.php b/src/Client/CallbackService.php
index 2073d78..ce6c90c 100644
--- a/src/Client/CallbackService.php
+++ b/src/Client/CallbackService.php
@@ -2,12 +2,12 @@
declare(strict_types=1);
-namespace EsewaPayment\Client;
+namespace Sujip\Esewa\Client;
-use EsewaPayment\Domain\Verification\CallbackPayload;
-use EsewaPayment\Domain\Verification\CallbackVerification;
-use EsewaPayment\Domain\Verification\VerificationExpectation;
-use EsewaPayment\Service\CallbackVerifier;
+use Sujip\Esewa\Domain\Verification\CallbackPayload;
+use Sujip\Esewa\Domain\Verification\CallbackVerification;
+use Sujip\Esewa\Domain\Verification\VerificationExpectation;
+use Sujip\Esewa\Service\CallbackVerifier;
final class CallbackService
{
diff --git a/src/Client/CheckoutService.php b/src/Client/CheckoutService.php
index 1fa1311..a177422 100644
--- a/src/Client/CheckoutService.php
+++ b/src/Client/CheckoutService.php
@@ -2,14 +2,14 @@
declare(strict_types=1);
-namespace EsewaPayment\Client;
+namespace Sujip\Esewa\Client;
-use EsewaPayment\Config\EndpointResolver;
-use EsewaPayment\Config\GatewayConfig;
-use EsewaPayment\Domain\Checkout\CheckoutIntent;
-use EsewaPayment\Domain\Checkout\CheckoutPayload;
-use EsewaPayment\Domain\Checkout\CheckoutRequest;
-use EsewaPayment\Service\SignatureService;
+use Sujip\Esewa\Config\EndpointResolver;
+use Sujip\Esewa\Config\GatewayConfig;
+use Sujip\Esewa\Domain\Checkout\CheckoutIntent;
+use Sujip\Esewa\Domain\Checkout\CheckoutPayload;
+use Sujip\Esewa\Domain\Checkout\CheckoutRequest;
+use Sujip\Esewa\Service\SignatureService;
final class CheckoutService
{
@@ -25,17 +25,17 @@ public function createIntent(CheckoutRequest $request): CheckoutIntent
$totalAmount = $request->totalAmount();
$signature = $this->signatures->generate(
$totalAmount,
- $request->transactionUuid,
- $request->productCode,
+ $request->transactionUuid->value(),
+ $request->productCode->value(),
$request->signedFieldNames,
);
$payload = new CheckoutPayload(
- amount: number_format((float) $request->amount, 2, '.', ''),
- taxAmount: number_format((float) $request->taxAmount, 2, '.', ''),
- serviceCharge: number_format((float) $request->serviceCharge, 2, '.', ''),
- deliveryCharge: number_format((float) $request->deliveryCharge, 2, '.', ''),
- totalAmount: $totalAmount,
+ amount: $request->amount,
+ taxAmount: $request->taxAmount,
+ serviceCharge: $request->serviceCharge,
+ deliveryCharge: $request->deliveryCharge,
+ totalAmount: \Sujip\Esewa\ValueObject\Amount::fromString($totalAmount),
transactionUuid: $request->transactionUuid,
productCode: $request->productCode,
successUrl: $request->successUrl,
diff --git a/src/Client/EsewaClient.php b/src/Client/EsewaClient.php
index b47ece3..3b54cca 100644
--- a/src/Client/EsewaClient.php
+++ b/src/Client/EsewaClient.php
@@ -2,14 +2,14 @@
declare(strict_types=1);
-namespace EsewaPayment\Client;
-
-use EsewaPayment\Config\ClientOptions;
-use EsewaPayment\Config\EndpointResolver;
-use EsewaPayment\Config\GatewayConfig;
-use EsewaPayment\Contracts\TransportInterface;
-use EsewaPayment\Service\CallbackVerifier;
-use EsewaPayment\Service\SignatureService;
+namespace Sujip\Esewa\Client;
+
+use Sujip\Esewa\Config\ClientOptions;
+use Sujip\Esewa\Config\EndpointResolver;
+use Sujip\Esewa\Config\GatewayConfig;
+use Sujip\Esewa\Contracts\TransportInterface;
+use Sujip\Esewa\Service\CallbackVerifier;
+use Sujip\Esewa\Service\SignatureService;
final class EsewaClient
{
diff --git a/src/Client/TransactionService.php b/src/Client/TransactionService.php
index b332365..e854702 100644
--- a/src/Client/TransactionService.php
+++ b/src/Client/TransactionService.php
@@ -2,16 +2,16 @@
declare(strict_types=1);
-namespace EsewaPayment\Client;
+namespace Sujip\Esewa\Client;
-use EsewaPayment\Config\ClientOptions;
-use EsewaPayment\Config\EndpointResolver;
-use EsewaPayment\Config\GatewayConfig;
-use EsewaPayment\Contracts\TransportInterface;
-use EsewaPayment\Domain\Transaction\TransactionStatus;
-use EsewaPayment\Domain\Transaction\TransactionStatusPayload;
-use EsewaPayment\Domain\Transaction\TransactionStatusRequest;
-use EsewaPayment\Exception\TransportException;
+use Sujip\Esewa\Config\ClientOptions;
+use Sujip\Esewa\Config\EndpointResolver;
+use Sujip\Esewa\Config\GatewayConfig;
+use Sujip\Esewa\Contracts\TransportInterface;
+use Sujip\Esewa\Domain\Transaction\TransactionStatus;
+use Sujip\Esewa\Domain\Transaction\TransactionStatusPayload;
+use Sujip\Esewa\Domain\Transaction\TransactionStatusRequest;
+use Sujip\Esewa\Exception\TransportException;
final class TransactionService
{
@@ -25,11 +25,6 @@ public function __construct(
public function fetchStatus(TransactionStatusRequest $query): TransactionStatus
{
- $this->options->logger->info('eSewa status check started.', [
- 'event' => 'esewa.status.started',
- 'transaction_uuid' => $query->transactionUuid,
- ]);
-
$attempt = 0;
while (true) {
@@ -37,45 +32,25 @@ public function fetchStatus(TransactionStatusRequest $query): TransactionStatus
$payload = $this->transport->get(
$this->endpoints->statusCheckUrl($this->config),
[
- 'product_code' => $query->productCode,
- 'total_amount' => $query->totalAmount,
- 'transaction_uuid' => $query->transactionUuid,
+ 'product_code' => $query->productCode->value(),
+ 'total_amount' => $query->totalAmount->value(),
+ 'transaction_uuid' => $query->transactionUuid->value(),
]
);
$result = TransactionStatusPayload::fromArray($payload)->toResult();
- $this->options->logger->info('eSewa status check completed.', [
- 'event' => 'esewa.status.completed',
- 'transaction_uuid' => $query->transactionUuid,
- 'status' => $result->status->value,
- ]);
-
return $result;
} catch (TransportException $exception) {
- if ($attempt >= $this->options->maxStatusRetries) {
- $this->options->logger->error('eSewa status check failed after retries.', [
- 'event' => 'esewa.status.failed',
- 'transaction_uuid' => $query->transactionUuid,
- 'attempt' => $attempt,
- 'error' => $exception->getMessage(),
- ]);
-
+ if (!$this->options->retryPolicy->shouldRetry($attempt, $exception)) {
throw $exception;
}
+ $delayUs = $this->options->retryPolicy->delayUs($attempt, $exception);
++$attempt;
- $this->options->logger->warning('eSewa status check retry scheduled.', [
- 'event' => 'esewa.status.retry',
- 'transaction_uuid' => $query->transactionUuid,
- 'attempt' => $attempt,
- 'max_retries' => $this->options->maxStatusRetries,
- 'error' => $exception->getMessage(),
- ]);
-
- if ($this->options->statusRetryDelayMs > 0) {
- usleep($this->options->statusRetryDelayMs * 1000);
+ if ($delayUs > 0) {
+ usleep($delayUs);
}
}
}
diff --git a/src/Config/ClientOptions.php b/src/Config/ClientOptions.php
index d618054..905998d 100644
--- a/src/Config/ClientOptions.php
+++ b/src/Config/ClientOptions.php
@@ -2,21 +2,27 @@
declare(strict_types=1);
-namespace EsewaPayment\Config;
+namespace Sujip\Esewa\Config;
-use EsewaPayment\Contracts\IdempotencyStoreInterface;
-use EsewaPayment\Infrastructure\Idempotency\NullIdempotencyStore;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
+use Sujip\Esewa\Contracts\ClockInterface;
+use Sujip\Esewa\Contracts\IdempotencyStoreInterface;
+use Sujip\Esewa\Contracts\RetryPolicyInterface;
+use Sujip\Esewa\Infrastructure\Idempotency\NullIdempotencyStore;
+use Sujip\Esewa\Support\FixedDelayRetryPolicy;
+use Sujip\Esewa\Support\SystemClock;
-final class ClientOptions
+final readonly class ClientOptions
{
+ public readonly RetryPolicyInterface $retryPolicy;
+ public readonly ClockInterface $clock;
+
public function __construct(
public readonly int $maxStatusRetries = 2,
public readonly int $statusRetryDelayMs = 150,
public readonly bool $preventCallbackReplay = true,
public readonly IdempotencyStoreInterface $idempotencyStore = new NullIdempotencyStore(),
- public readonly LoggerInterface $logger = new NullLogger(),
+ ?RetryPolicyInterface $retryPolicy = null,
+ ?ClockInterface $clock = null,
) {
if ($maxStatusRetries < 0) {
throw new \InvalidArgumentException('maxStatusRetries cannot be negative.');
@@ -25,5 +31,11 @@ public function __construct(
if ($statusRetryDelayMs < 0) {
throw new \InvalidArgumentException('statusRetryDelayMs cannot be negative.');
}
+
+ $this->retryPolicy = $retryPolicy ?? new FixedDelayRetryPolicy(
+ maxRetries: $maxStatusRetries,
+ delayUs: $statusRetryDelayMs * 1000,
+ );
+ $this->clock = $clock ?? new SystemClock();
}
}
diff --git a/src/Config/EndpointResolver.php b/src/Config/EndpointResolver.php
index f80fdf0..2abc7de 100644
--- a/src/Config/EndpointResolver.php
+++ b/src/Config/EndpointResolver.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace EsewaPayment\Config;
+namespace Sujip\Esewa\Config;
final class EndpointResolver
{
diff --git a/src/Config/Environment.php b/src/Config/Environment.php
index b373d45..b81e44a 100644
--- a/src/Config/Environment.php
+++ b/src/Config/Environment.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace EsewaPayment\Config;
+namespace Sujip\Esewa\Config;
enum Environment: string
{
diff --git a/src/Config/GatewayConfig.php b/src/Config/GatewayConfig.php
index 82edcfb..31db477 100644
--- a/src/Config/GatewayConfig.php
+++ b/src/Config/GatewayConfig.php
@@ -2,9 +2,9 @@
declare(strict_types=1);
-namespace EsewaPayment\Config;
+namespace Sujip\Esewa\Config;
-final class GatewayConfig
+final readonly class GatewayConfig
{
public function __construct(
public readonly string $merchantCode,
diff --git a/src/Contracts/Arrayable.php b/src/Contracts/Arrayable.php
new file mode 100644
index 0000000..cc643b1
--- /dev/null
+++ b/src/Contracts/Arrayable.php
@@ -0,0 +1,13 @@
+
+ */
+ public function toArray(): array;
+}
diff --git a/src/Contracts/ClockInterface.php b/src/Contracts/ClockInterface.php
new file mode 100644
index 0000000..b979cf6
--- /dev/null
+++ b/src/Contracts/ClockInterface.php
@@ -0,0 +1,10 @@
+ $data
+ */
+ public static function fromArray(array $data): static;
+}
diff --git a/src/Contracts/IdempotencyStoreInterface.php b/src/Contracts/IdempotencyStoreInterface.php
index fd6842f..abfc85d 100644
--- a/src/Contracts/IdempotencyStoreInterface.php
+++ b/src/Contracts/IdempotencyStoreInterface.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace EsewaPayment\Contracts;
+namespace Sujip\Esewa\Contracts;
interface IdempotencyStoreInterface
{
diff --git a/src/Contracts/RetryPolicyInterface.php b/src/Contracts/RetryPolicyInterface.php
new file mode 100644
index 0000000..e56fa1d
--- /dev/null
+++ b/src/Contracts/RetryPolicyInterface.php
@@ -0,0 +1,14 @@
+ $this->amount,
- 'tax_amount' => $this->taxAmount,
- 'product_service_charge' => $this->serviceCharge,
- 'product_delivery_charge' => $this->deliveryCharge,
- 'total_amount' => $this->totalAmount,
- 'transaction_uuid' => $this->transactionUuid,
- 'product_code' => $this->productCode,
+ 'amount' => $this->amount->value(),
+ 'tax_amount' => $this->taxAmount->value(),
+ 'product_service_charge' => $this->serviceCharge->value(),
+ 'product_delivery_charge' => $this->deliveryCharge->value(),
+ 'total_amount' => $this->totalAmount->value(),
+ 'transaction_uuid' => $this->transactionUuid->value(),
+ 'product_code' => $this->productCode->value(),
'success_url' => $this->successUrl,
'failure_url' => $this->failureUrl,
'signed_field_names' => $this->signedFieldNames,
diff --git a/src/Domain/Checkout/CheckoutRequest.php b/src/Domain/Checkout/CheckoutRequest.php
index 1558b1d..60682b6 100644
--- a/src/Domain/Checkout/CheckoutRequest.php
+++ b/src/Domain/Checkout/CheckoutRequest.php
@@ -2,24 +2,48 @@
declare(strict_types=1);
-namespace EsewaPayment\Domain\Checkout;
+namespace Sujip\Esewa\Domain\Checkout;
-final class CheckoutRequest
+use Sujip\Esewa\Contracts\Arrayable;
+use Sujip\Esewa\Contracts\Hydratable;
+use Sujip\Esewa\ValueObject\Amount;
+use Sujip\Esewa\ValueObject\ProductCode;
+use Sujip\Esewa\ValueObject\TransactionUuid;
+
+final readonly class CheckoutRequest implements Arrayable, Hydratable
{
+ public readonly Amount $amount;
+ public readonly Amount $taxAmount;
+ public readonly Amount $serviceCharge;
+ public readonly Amount $deliveryCharge;
+ public readonly TransactionUuid $transactionUuid;
+ public readonly ProductCode $productCode;
+ public readonly string $successUrl;
+ public readonly string $failureUrl;
+ public readonly string $signedFieldNames;
+
public function __construct(
- public readonly string $amount,
- public readonly string $taxAmount,
- public readonly string $serviceCharge,
- public readonly string $deliveryCharge,
- public readonly string $transactionUuid,
- public readonly string $productCode,
- public readonly string $successUrl,
- public readonly string $failureUrl,
- public readonly string $signedFieldNames = 'total_amount,transaction_uuid,product_code',
+ string|Amount $amount,
+ string|Amount $taxAmount,
+ string|Amount $serviceCharge,
+ string|Amount $deliveryCharge,
+ string|TransactionUuid $transactionUuid,
+ string|ProductCode $productCode,
+ string $successUrl,
+ string $failureUrl,
+ string $signedFieldNames = 'total_amount,transaction_uuid,product_code',
) {
+ $this->amount = self::normalizeAmount($amount);
+ $this->taxAmount = self::normalizeAmount($taxAmount);
+ $this->serviceCharge = self::normalizeAmount($serviceCharge);
+ $this->deliveryCharge = self::normalizeAmount($deliveryCharge);
+ $this->transactionUuid = self::normalizeTransactionUuid($transactionUuid);
+ $this->productCode = self::normalizeProductCode($productCode);
+ $this->successUrl = $successUrl;
+ $this->failureUrl = $failureUrl;
+ $this->signedFieldNames = $signedFieldNames;
+
foreach ([
- 'transactionUuid' => $transactionUuid,
- 'productCode' => $productCode,
'successUrl' => $successUrl,
'failureUrl' => $failureUrl,
] as $field => $value) {
@@ -29,13 +53,87 @@ public function __construct(
}
}
+ public static function make(
+ string|Amount $amount,
+ string|Amount $taxAmount,
+ string|Amount $serviceCharge,
+ string|Amount $deliveryCharge,
+ string|TransactionUuid $transactionUuid,
+ string|ProductCode $productCode,
+ string $successUrl,
+ string $failureUrl,
+ string $signedFieldNames = 'total_amount,transaction_uuid,product_code',
+ ): self {
+ return new self(
+ amount: self::normalizeAmount($amount),
+ taxAmount: self::normalizeAmount($taxAmount),
+ serviceCharge: self::normalizeAmount($serviceCharge),
+ deliveryCharge: self::normalizeAmount($deliveryCharge),
+ transactionUuid: self::normalizeTransactionUuid($transactionUuid),
+ productCode: self::normalizeProductCode($productCode),
+ successUrl: $successUrl,
+ failureUrl: $failureUrl,
+ signedFieldNames: $signedFieldNames,
+ );
+ }
+
+ /**
+ * @param array $data
+ */
+ public static function fromArray(array $data): static
+ {
+ return self::make(
+ amount: (string) ($data['amount'] ?? ''),
+ taxAmount: (string) ($data['tax_amount'] ?? $data['taxAmount'] ?? '0'),
+ serviceCharge: (string) ($data['service_charge'] ?? $data['serviceCharge'] ?? '0'),
+ deliveryCharge: (string) ($data['delivery_charge'] ?? $data['deliveryCharge'] ?? '0'),
+ transactionUuid: (string) ($data['transaction_uuid'] ?? $data['transactionUuid'] ?? ''),
+ productCode: (string) ($data['product_code'] ?? $data['productCode'] ?? ''),
+ successUrl: (string) ($data['success_url'] ?? $data['successUrl'] ?? ''),
+ failureUrl: (string) ($data['failure_url'] ?? $data['failureUrl'] ?? ''),
+ signedFieldNames: (string) ($data['signed_field_names'] ?? $data['signedFieldNames'] ?? 'total_amount,transaction_uuid,product_code'),
+ );
+ }
+
public function totalAmount(): string
{
- $total = (float) $this->amount
- + (float) $this->taxAmount
- + (float) $this->serviceCharge
- + (float) $this->deliveryCharge;
+ return $this->amount
+ ->add($this->taxAmount)
+ ->add($this->serviceCharge)
+ ->add($this->deliveryCharge)
+ ->value();
+ }
- return number_format($total, 2, '.', '');
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'amount' => $this->amount->value(),
+ 'tax_amount' => $this->taxAmount->value(),
+ 'service_charge' => $this->serviceCharge->value(),
+ 'delivery_charge' => $this->deliveryCharge->value(),
+ 'transaction_uuid' => $this->transactionUuid->value(),
+ 'product_code' => $this->productCode->value(),
+ 'success_url' => $this->successUrl,
+ 'failure_url' => $this->failureUrl,
+ 'signed_field_names' => $this->signedFieldNames,
+ ];
+ }
+
+ private static function normalizeAmount(string|Amount $value): Amount
+ {
+ return $value instanceof Amount ? $value : Amount::fromString($value);
+ }
+
+ private static function normalizeTransactionUuid(string|TransactionUuid $value): TransactionUuid
+ {
+ return $value instanceof TransactionUuid ? $value : TransactionUuid::fromString($value);
+ }
+
+ private static function normalizeProductCode(string|ProductCode $value): ProductCode
+ {
+ return $value instanceof ProductCode ? $value : ProductCode::fromString($value);
}
}
diff --git a/src/Domain/Transaction/PaymentStatus.php b/src/Domain/Transaction/PaymentStatus.php
index 393b8c6..1f177d7 100644
--- a/src/Domain/Transaction/PaymentStatus.php
+++ b/src/Domain/Transaction/PaymentStatus.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace EsewaPayment\Domain\Transaction;
+namespace Sujip\Esewa\Domain\Transaction;
enum PaymentStatus: string
{
diff --git a/src/Domain/Transaction/TransactionStatus.php b/src/Domain/Transaction/TransactionStatus.php
index 43e54fe..3f355f3 100644
--- a/src/Domain/Transaction/TransactionStatus.php
+++ b/src/Domain/Transaction/TransactionStatus.php
@@ -2,16 +2,19 @@
declare(strict_types=1);
-namespace EsewaPayment\Domain\Transaction;
+namespace Sujip\Esewa\Domain\Transaction;
-final class TransactionStatus
+use Sujip\Esewa\Contracts\Arrayable;
+use Sujip\Esewa\ValueObject\ReferenceId;
+
+final readonly class TransactionStatus implements Arrayable
{
/**
* @param array $raw
*/
public function __construct(
public readonly PaymentStatus $status,
- public readonly ?string $referenceId,
+ public readonly ?ReferenceId $referenceId,
public readonly array $raw,
) {
}
@@ -20,4 +23,16 @@ public function isSuccessful(): bool
{
return $this->status === PaymentStatus::COMPLETE;
}
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'status' => $this->status->value,
+ 'reference_id' => $this->referenceId?->value(),
+ 'raw' => $this->raw,
+ ];
+ }
}
diff --git a/src/Domain/Transaction/TransactionStatusPayload.php b/src/Domain/Transaction/TransactionStatusPayload.php
index 112b99b..ced91ac 100644
--- a/src/Domain/Transaction/TransactionStatusPayload.php
+++ b/src/Domain/Transaction/TransactionStatusPayload.php
@@ -2,26 +2,32 @@
declare(strict_types=1);
-namespace EsewaPayment\Domain\Transaction;
+namespace Sujip\Esewa\Domain\Transaction;
-final class TransactionStatusPayload
+use Sujip\Esewa\Contracts\Arrayable;
+use Sujip\Esewa\Contracts\Hydratable;
+use Sujip\Esewa\ValueObject\ReferenceId;
+
+final readonly class TransactionStatusPayload implements Arrayable, Hydratable
{
/**
* @param array $raw
*/
public function __construct(
public readonly PaymentStatus $status,
- public readonly ?string $referenceId,
+ public readonly ?ReferenceId $referenceId,
public readonly array $raw,
) {
}
/** @param array $raw */
- public static function fromArray(array $raw): self
+ public static function fromArray(array $raw): static
{
- return new self(
+ return new static(
status: PaymentStatus::fromValue(isset($raw['status']) ? (string) $raw['status'] : null),
- referenceId: isset($raw['ref_id']) ? (string) $raw['ref_id'] : null,
+ referenceId: isset($raw['ref_id']) && (string) $raw['ref_id'] !== ''
+ ? ReferenceId::fromString((string) $raw['ref_id'])
+ : null,
raw: $raw,
);
}
@@ -30,4 +36,16 @@ public function toResult(): TransactionStatus
{
return new TransactionStatus($this->status, $this->referenceId, $this->raw);
}
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'status' => $this->status->value,
+ 'ref_id' => $this->referenceId?->value(),
+ 'raw' => $this->raw,
+ ];
+ }
}
diff --git a/src/Domain/Transaction/TransactionStatusRequest.php b/src/Domain/Transaction/TransactionStatusRequest.php
index c8f5fbe..e77b0c6 100644
--- a/src/Domain/Transaction/TransactionStatusRequest.php
+++ b/src/Domain/Transaction/TransactionStatusRequest.php
@@ -2,17 +2,63 @@
declare(strict_types=1);
-namespace EsewaPayment\Domain\Transaction;
+namespace Sujip\Esewa\Domain\Transaction;
-final class TransactionStatusRequest
+use Sujip\Esewa\Contracts\Arrayable;
+use Sujip\Esewa\Contracts\Hydratable;
+use Sujip\Esewa\ValueObject\Amount;
+use Sujip\Esewa\ValueObject\ProductCode;
+use Sujip\Esewa\ValueObject\TransactionUuid;
+
+final readonly class TransactionStatusRequest implements Arrayable, Hydratable
{
+ public readonly TransactionUuid $transactionUuid;
+ public readonly Amount $totalAmount;
+ public readonly ProductCode $productCode;
+
public function __construct(
- public readonly string $transactionUuid,
- public readonly string $totalAmount,
- public readonly string $productCode,
+ string|TransactionUuid $transactionUuid,
+ string|Amount $totalAmount,
+ string|ProductCode $productCode,
) {
- if ($transactionUuid === '' || $totalAmount === '' || $productCode === '') {
- throw new \InvalidArgumentException('transactionUuid, totalAmount and productCode are required.');
- }
+ $this->transactionUuid = $transactionUuid instanceof TransactionUuid ? $transactionUuid : TransactionUuid::fromString($transactionUuid);
+ $this->totalAmount = $totalAmount instanceof Amount ? $totalAmount : Amount::fromString($totalAmount);
+ $this->productCode = $productCode instanceof ProductCode ? $productCode : ProductCode::fromString($productCode);
+ }
+
+ public static function make(
+ string|TransactionUuid $transactionUuid,
+ string|Amount $totalAmount,
+ string|ProductCode $productCode,
+ ): self {
+ return new self(
+ transactionUuid: $transactionUuid,
+ totalAmount: $totalAmount,
+ productCode: $productCode,
+ );
+ }
+
+ /**
+ * @param array $data
+ */
+ public static function fromArray(array $data): static
+ {
+ return self::make(
+ transactionUuid: (string) ($data['transaction_uuid'] ?? $data['transactionUuid'] ?? ''),
+ totalAmount: (string) ($data['total_amount'] ?? $data['totalAmount'] ?? ''),
+ productCode: (string) ($data['product_code'] ?? $data['productCode'] ?? ''),
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'transaction_uuid' => $this->transactionUuid->value(),
+ 'total_amount' => $this->totalAmount->value(),
+ 'product_code' => $this->productCode->value(),
+ ];
}
}
diff --git a/src/Domain/Verification/CallbackData.php b/src/Domain/Verification/CallbackData.php
index 9fa6036..1537031 100644
--- a/src/Domain/Verification/CallbackData.php
+++ b/src/Domain/Verification/CallbackData.php
@@ -2,29 +2,35 @@
declare(strict_types=1);
-namespace EsewaPayment\Domain\Verification;
+namespace Sujip\Esewa\Domain\Verification;
-use EsewaPayment\Domain\Transaction\PaymentStatus;
-use EsewaPayment\Exception\InvalidPayloadException;
+use Sujip\Esewa\Contracts\Arrayable;
+use Sujip\Esewa\Contracts\Hydratable;
+use Sujip\Esewa\Domain\Transaction\PaymentStatus;
+use Sujip\Esewa\Exception\InvalidPayloadException;
+use Sujip\Esewa\ValueObject\Amount;
+use Sujip\Esewa\ValueObject\ProductCode;
+use Sujip\Esewa\ValueObject\ReferenceId;
+use Sujip\Esewa\ValueObject\TransactionUuid;
-final class CallbackData
+final readonly class CallbackData implements Arrayable, Hydratable
{
/**
* @param array $raw
*/
public function __construct(
- public readonly string $totalAmount,
- public readonly string $transactionUuid,
- public readonly string $productCode,
+ public readonly Amount $totalAmount,
+ public readonly TransactionUuid $transactionUuid,
+ public readonly ProductCode $productCode,
public readonly string $signedFieldNames,
public readonly PaymentStatus $status,
- public readonly ?string $transactionCode,
+ public readonly ?ReferenceId $transactionCode,
public readonly array $raw,
) {
}
/** @param array $data */
- public static function fromArray(array $data): self
+ public static function fromArray(array $data): static
{
$totalAmount = (string) ($data['total_amount'] ?? '');
$transactionUuid = (string) ($data['transaction_uuid'] ?? '');
@@ -34,14 +40,31 @@ public static function fromArray(array $data): self
throw new InvalidPayloadException('Callback data is missing required fields.');
}
- return new self(
- totalAmount: $totalAmount,
- transactionUuid: $transactionUuid,
- productCode: $productCode,
+ return new static(
+ totalAmount: Amount::fromString($totalAmount),
+ transactionUuid: TransactionUuid::fromString($transactionUuid),
+ productCode: ProductCode::fromString($productCode),
signedFieldNames: (string) ($data['signed_field_names'] ?? 'total_amount,transaction_uuid,product_code'),
status: PaymentStatus::fromValue((string) ($data['status'] ?? null)),
- transactionCode: isset($data['transaction_code']) ? (string) $data['transaction_code'] : null,
+ transactionCode: isset($data['transaction_code']) && (string) $data['transaction_code'] !== ''
+ ? ReferenceId::fromString((string) $data['transaction_code'])
+ : null,
raw: $data,
);
}
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'total_amount' => $this->totalAmount->value(),
+ 'transaction_uuid' => $this->transactionUuid->value(),
+ 'product_code' => $this->productCode->value(),
+ 'signed_field_names' => $this->signedFieldNames,
+ 'status' => $this->status->value,
+ 'transaction_code' => $this->transactionCode?->value(),
+ ];
+ }
}
diff --git a/src/Domain/Verification/CallbackPayload.php b/src/Domain/Verification/CallbackPayload.php
index 240f731..7c1d6d9 100644
--- a/src/Domain/Verification/CallbackPayload.php
+++ b/src/Domain/Verification/CallbackPayload.php
@@ -2,11 +2,11 @@
declare(strict_types=1);
-namespace EsewaPayment\Domain\Verification;
+namespace Sujip\Esewa\Domain\Verification;
-use EsewaPayment\Exception\InvalidPayloadException;
+use Sujip\Esewa\Exception\InvalidPayloadException;
-final class CallbackPayload
+final readonly class CallbackPayload
{
public function __construct(
public readonly string $data,
diff --git a/src/Domain/Verification/CallbackVerification.php b/src/Domain/Verification/CallbackVerification.php
index be7c7f0..aa293f9 100644
--- a/src/Domain/Verification/CallbackVerification.php
+++ b/src/Domain/Verification/CallbackVerification.php
@@ -2,19 +2,22 @@
declare(strict_types=1);
-namespace EsewaPayment\Domain\Verification;
+namespace Sujip\Esewa\Domain\Verification;
-use EsewaPayment\Domain\Transaction\PaymentStatus;
+use Sujip\Esewa\Contracts\Arrayable;
+use Sujip\Esewa\Domain\Transaction\PaymentStatus;
+use Sujip\Esewa\ValueObject\ReferenceId;
-final class CallbackVerification
+final readonly class CallbackVerification implements Arrayable
{
/**
* @param array $raw
*/
public function __construct(
+ public readonly VerificationState $state,
public readonly bool $valid,
public readonly PaymentStatus $status,
- public readonly ?string $referenceId,
+ public readonly ?ReferenceId $referenceId,
public readonly string $message,
public readonly array $raw,
) {
@@ -24,4 +27,24 @@ public function isSuccessful(): bool
{
return $this->valid && $this->status === PaymentStatus::COMPLETE;
}
+
+ public function isReplayed(): bool
+ {
+ return $this->state === VerificationState::REPLAYED;
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'state' => $this->state->value,
+ 'valid' => $this->valid,
+ 'status' => $this->status->value,
+ 'reference_id' => $this->referenceId?->value(),
+ 'message' => $this->message,
+ 'raw' => $this->raw,
+ ];
+ }
}
diff --git a/src/Domain/Verification/VerificationExpectation.php b/src/Domain/Verification/VerificationExpectation.php
index 682ed23..e775e22 100644
--- a/src/Domain/Verification/VerificationExpectation.php
+++ b/src/Domain/Verification/VerificationExpectation.php
@@ -2,15 +2,78 @@
declare(strict_types=1);
-namespace EsewaPayment\Domain\Verification;
+namespace Sujip\Esewa\Domain\Verification;
-final class VerificationExpectation
+use Sujip\Esewa\Contracts\Arrayable;
+use Sujip\Esewa\Contracts\Hydratable;
+use Sujip\Esewa\ValueObject\Amount;
+use Sujip\Esewa\ValueObject\ProductCode;
+use Sujip\Esewa\ValueObject\ReferenceId;
+use Sujip\Esewa\ValueObject\TransactionUuid;
+
+final readonly class VerificationExpectation implements Arrayable, Hydratable
{
+ public readonly Amount $totalAmount;
+ public readonly TransactionUuid $transactionUuid;
+ public readonly ProductCode $productCode;
+ public readonly ?ReferenceId $referenceId;
+
public function __construct(
- public readonly string $totalAmount,
- public readonly string $transactionUuid,
- public readonly string $productCode,
- public readonly ?string $referenceId = null,
+ string|Amount $totalAmount,
+ string|TransactionUuid $transactionUuid,
+ string|ProductCode $productCode,
+ string|ReferenceId|null $referenceId = null,
) {
+ $this->totalAmount = $totalAmount instanceof Amount ? $totalAmount : Amount::fromString($totalAmount);
+ $this->transactionUuid = $transactionUuid instanceof TransactionUuid ? $transactionUuid : TransactionUuid::fromString($transactionUuid);
+ $this->productCode = $productCode instanceof ProductCode ? $productCode : ProductCode::fromString($productCode);
+ $this->referenceId = $referenceId instanceof ReferenceId || $referenceId === null ? $referenceId : ReferenceId::fromString($referenceId);
+ }
+
+ public static function make(
+ string|Amount $totalAmount,
+ string|TransactionUuid $transactionUuid,
+ string|ProductCode $productCode,
+ string|ReferenceId|null $referenceId = null,
+ ): self {
+ return new self(
+ totalAmount: $totalAmount,
+ transactionUuid: $transactionUuid,
+ productCode: $productCode,
+ referenceId: $referenceId,
+ );
+ }
+
+ /**
+ * @param array $data
+ */
+ public static function fromArray(array $data): static
+ {
+ return self::make(
+ totalAmount: (string) ($data['total_amount'] ?? $data['totalAmount'] ?? ''),
+ transactionUuid: (string) ($data['transaction_uuid'] ?? $data['transactionUuid'] ?? ''),
+ productCode: (string) ($data['product_code'] ?? $data['productCode'] ?? ''),
+ referenceId: isset($data['reference_id']) || isset($data['referenceId'])
+ ? (string) ($data['reference_id'] ?? $data['referenceId'])
+ : null,
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ $payload = [
+ 'total_amount' => $this->totalAmount->value(),
+ 'transaction_uuid' => $this->transactionUuid->value(),
+ 'product_code' => $this->productCode->value(),
+ ];
+
+ if ($this->referenceId !== null) {
+ $payload['reference_id'] = $this->referenceId->value();
+ }
+
+ return $payload;
}
}
diff --git a/src/Domain/Verification/VerificationState.php b/src/Domain/Verification/VerificationState.php
new file mode 100644
index 0000000..28f0af3
--- /dev/null
+++ b/src/Domain/Verification/VerificationState.php
@@ -0,0 +1,12 @@
+ttlSeconds < 1) {
+ throw new \InvalidArgumentException('ttlSeconds must be at least 1.');
+ }
+
+ if (!is_dir($this->directory) && !@mkdir($this->directory, 0777, true) && !is_dir($this->directory)) {
+ throw new \RuntimeException(sprintf('Unable to create idempotency directory: %s', $this->directory));
+ }
+ }
+
+ public function has(string $key): bool
+ {
+ $path = $this->path($key);
+
+ if (!is_file($path)) {
+ return false;
+ }
+
+ $expiresAt = (int) trim((string) file_get_contents($path));
+ if ($expiresAt <= $this->clock->now()->getTimestamp()) {
+ @unlink($path);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public function put(string $key): void
+ {
+ $expiresAt = $this->clock->now()->getTimestamp() + $this->ttlSeconds;
+ file_put_contents($this->path($key), (string) $expiresAt, LOCK_EX);
+ }
+
+ private function path(string $key): string
+ {
+ return rtrim($this->directory, '/').'/'.hash('sha256', $key).'.lock';
+ }
+}
diff --git a/src/Infrastructure/Idempotency/InMemoryIdempotencyStore.php b/src/Infrastructure/Idempotency/InMemoryIdempotencyStore.php
index 8eff353..ab787cf 100644
--- a/src/Infrastructure/Idempotency/InMemoryIdempotencyStore.php
+++ b/src/Infrastructure/Idempotency/InMemoryIdempotencyStore.php
@@ -2,9 +2,9 @@
declare(strict_types=1);
-namespace EsewaPayment\Infrastructure\Idempotency;
+namespace Sujip\Esewa\Infrastructure\Idempotency;
-use EsewaPayment\Contracts\IdempotencyStoreInterface;
+use Sujip\Esewa\Contracts\IdempotencyStoreInterface;
final class InMemoryIdempotencyStore implements IdempotencyStoreInterface
{
diff --git a/src/Infrastructure/Idempotency/NullIdempotencyStore.php b/src/Infrastructure/Idempotency/NullIdempotencyStore.php
index a8782b3..a35c5a2 100644
--- a/src/Infrastructure/Idempotency/NullIdempotencyStore.php
+++ b/src/Infrastructure/Idempotency/NullIdempotencyStore.php
@@ -2,9 +2,9 @@
declare(strict_types=1);
-namespace EsewaPayment\Infrastructure\Idempotency;
+namespace Sujip\Esewa\Infrastructure\Idempotency;
-use EsewaPayment\Contracts\IdempotencyStoreInterface;
+use Sujip\Esewa\Contracts\IdempotencyStoreInterface;
final class NullIdempotencyStore implements IdempotencyStoreInterface
{
diff --git a/src/Infrastructure/Idempotency/PdoIdempotencyStore.php b/src/Infrastructure/Idempotency/PdoIdempotencyStore.php
new file mode 100644
index 0000000..0bbf0ca
--- /dev/null
+++ b/src/Infrastructure/Idempotency/PdoIdempotencyStore.php
@@ -0,0 +1,65 @@
+ttlSeconds < 1) {
+ throw new \InvalidArgumentException('ttlSeconds must be at least 1.');
+ }
+
+ $this->initialize();
+ }
+
+ public function has(string $key): bool
+ {
+ $this->purgeExpired();
+
+ $statement = $this->pdo->prepare(sprintf('SELECT 1 FROM %s WHERE idempotency_key = :key LIMIT 1', $this->table));
+ $statement->execute(['key' => $key]);
+
+ return (bool) $statement->fetchColumn();
+ }
+
+ public function put(string $key): void
+ {
+ $statement = $this->pdo->prepare(sprintf(
+ 'INSERT OR REPLACE INTO %s (idempotency_key, expires_at) VALUES (:key, :expires_at)',
+ $this->table
+ ));
+
+ $statement->execute([
+ 'key' => $key,
+ 'expires_at' => $this->clock->now()->getTimestamp() + $this->ttlSeconds,
+ ]);
+ }
+
+ private function initialize(): void
+ {
+ $this->pdo->exec(sprintf(
+ 'CREATE TABLE IF NOT EXISTS %s (idempotency_key VARCHAR(255) PRIMARY KEY, expires_at INTEGER NOT NULL)',
+ $this->table
+ ));
+ }
+
+ private function purgeExpired(): void
+ {
+ $statement = $this->pdo->prepare(sprintf('DELETE FROM %s WHERE expires_at <= :expires_at', $this->table));
+ $statement->execute([
+ 'expires_at' => $this->clock->now()->getTimestamp(),
+ ]);
+ }
+}
diff --git a/src/Infrastructure/Transport/CurlTransport.php b/src/Infrastructure/Transport/CurlTransport.php
new file mode 100644
index 0000000..fa63e7b
--- /dev/null
+++ b/src/Infrastructure/Transport/CurlTransport.php
@@ -0,0 +1,75 @@
+timeoutSeconds < 1) {
+ throw new \InvalidArgumentException('timeoutSeconds must be at least 1.');
+ }
+ }
+
+ public function get(string $url, array $query = [], array $headers = []): array
+ {
+ if (!function_exists('curl_init')) {
+ throw new TransportException('ext-curl is required for CurlTransport.');
+ }
+
+ $fullUrl = $url;
+ if ($query !== []) {
+ $fullUrl .= (str_contains($url, '?') ? '&' : '?').http_build_query($query);
+ }
+
+ $curl = curl_init($fullUrl);
+ if ($curl === false) {
+ throw new TransportException('Failed to initialize curl transport.');
+ }
+
+ $normalizedHeaders = ['Accept: application/json'];
+ foreach ($headers as $name => $value) {
+ $normalizedHeaders[] = $name.': '.$value;
+ }
+
+ curl_setopt_array($curl, [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_TIMEOUT => $this->timeoutSeconds,
+ CURLOPT_HTTPHEADER => $normalizedHeaders,
+ ]);
+
+ $body = curl_exec($curl);
+ if ($body === false) {
+ $message = curl_error($curl);
+ curl_close($curl);
+
+ throw new TransportException('HTTP request failed: '.$message);
+ }
+
+ $statusCode = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
+ curl_close($curl);
+
+ if ($statusCode < 200 || $statusCode >= 300) {
+ throw new TransportException('Unexpected HTTP status: '.$statusCode);
+ }
+
+ if (!is_string($body)) {
+ throw new ApiErrorException('Unexpected non-string response from eSewa status API.');
+ }
+
+ $decoded = json_decode($body, true);
+ if (!is_array($decoded)) {
+ throw new ApiErrorException('Invalid JSON response from eSewa status API.');
+ }
+
+ return $decoded;
+ }
+}
diff --git a/src/Infrastructure/Transport/Psr18Transport.php b/src/Infrastructure/Transport/Psr18Transport.php
index ac3b01c..57dca61 100644
--- a/src/Infrastructure/Transport/Psr18Transport.php
+++ b/src/Infrastructure/Transport/Psr18Transport.php
@@ -2,11 +2,11 @@
declare(strict_types=1);
-namespace EsewaPayment\Infrastructure\Transport;
+namespace Sujip\Esewa\Infrastructure\Transport;
-use EsewaPayment\Contracts\TransportInterface;
-use EsewaPayment\Exception\ApiErrorException;
-use EsewaPayment\Exception\TransportException;
+use Sujip\Esewa\Contracts\TransportInterface;
+use Sujip\Esewa\Exception\ApiErrorException;
+use Sujip\Esewa\Exception\TransportException;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
diff --git a/src/Omnipay/Message/AbstractRequest.php b/src/Omnipay/Message/AbstractRequest.php
new file mode 100644
index 0000000..6fdf6f7
--- /dev/null
+++ b/src/Omnipay/Message/AbstractRequest.php
@@ -0,0 +1,171 @@
+getParameter('merchantCode');
+ }
+
+ public function setMerchantCode(string $value): self
+ {
+ return $this->setParameter('merchantCode', $value);
+ }
+
+ public function getSecretKey(): string
+ {
+ return (string) $this->getParameter('secretKey');
+ }
+
+ public function setSecretKey(string $value): self
+ {
+ return $this->setParameter('secretKey', $value);
+ }
+
+ public function getTaxAmount(): string
+ {
+ return (string) $this->getParameter('taxAmount');
+ }
+
+ public function setTaxAmount(string $value): self
+ {
+ return $this->setParameter('taxAmount', $value);
+ }
+
+ public function getServiceCharge(): string
+ {
+ return (string) $this->getParameter('serviceCharge');
+ }
+
+ public function setServiceCharge(string $value): self
+ {
+ return $this->setParameter('serviceCharge', $value);
+ }
+
+ public function getDeliveryCharge(): string
+ {
+ return (string) $this->getParameter('deliveryCharge');
+ }
+
+ public function setDeliveryCharge(string $value): self
+ {
+ return $this->setParameter('deliveryCharge', $value);
+ }
+
+ public function getTotalAmount(): string
+ {
+ return (string) $this->getParameter('totalAmount');
+ }
+
+ public function setTotalAmount(string $value): self
+ {
+ return $this->setParameter('totalAmount', $value);
+ }
+
+ public function getProductCode(): string
+ {
+ return (string) $this->getParameter('productCode');
+ }
+
+ public function setProductCode(string $value): self
+ {
+ return $this->setParameter('productCode', $value);
+ }
+
+ public function getFailureUrl(): string
+ {
+ return (string) $this->getParameter('failureUrl');
+ }
+
+ public function getFailedUrl(): string
+ {
+ return $this->getFailureUrl();
+ }
+
+ public function setFailureUrl(string $value): self
+ {
+ return $this->setParameter('failureUrl', $value);
+ }
+
+ public function setFailedUrl(string $value): self
+ {
+ return $this->setFailureUrl($value);
+ }
+
+ public function getReferenceNumber(): string
+ {
+ return (string) $this->getParameter('referenceNumber');
+ }
+
+ public function setReferenceNumber(string $value): self
+ {
+ return $this->setParameter('referenceNumber', $value);
+ }
+
+ public function getTransactionUuid(): string
+ {
+ return (string) $this->getParameter('transactionUuid');
+ }
+
+ public function setTransactionUuid(string $value): self
+ {
+ return $this->setParameter('transactionUuid', $value);
+ }
+
+ public function setTransport(TransportInterface $transport): self
+ {
+ return $this->setParameter('transport', $transport);
+ }
+
+ public function getTransport(): ?TransportInterface
+ {
+ $transport = $this->getParameter('transport');
+
+ return $transport instanceof TransportInterface ? $transport : null;
+ }
+
+ public function getTimeoutSeconds(): int
+ {
+ $timeout = $this->getParameter('timeoutSeconds');
+
+ if ($timeout === null) {
+ return 30;
+ }
+
+ return max(1, (int) $timeout);
+ }
+
+ public function setTimeoutSeconds(int $value): self
+ {
+ return $this->setParameter('timeoutSeconds', max(1, $value));
+ }
+
+ protected function gatewayConfig(): GatewayConfig
+ {
+ $productCode = $this->getProductCode();
+ if ($productCode === '') {
+ $productCode = $this->getMerchantCode();
+ $this->setProductCode($productCode);
+ }
+
+ return GatewayConfig::make(
+ merchantCode: $this->getMerchantCode(),
+ secretKey: $this->getSecretKey(),
+ environment: $this->getTestMode() ? Environment::UAT : Environment::PRODUCTION,
+ );
+ }
+
+ protected function transport(): TransportInterface
+ {
+ return $this->getTransport() ?? new CurlTransport($this->getTimeoutSeconds());
+ }
+}
diff --git a/src/Omnipay/Message/CompletePurchaseRequest.php b/src/Omnipay/Message/CompletePurchaseRequest.php
new file mode 100644
index 0000000..1b0b45f
--- /dev/null
+++ b/src/Omnipay/Message/CompletePurchaseRequest.php
@@ -0,0 +1,80 @@
+
+ */
+ public function getData(): array
+ {
+ $httpRequest = $this->httpRequest;
+ if (isset($httpRequest->query) && is_object($httpRequest->query) && method_exists($httpRequest->query, 'all')) {
+ /** @var array $query */
+ $query = $httpRequest->query->all();
+ if ($query !== []) {
+ return $query;
+ }
+ }
+
+ if (isset($httpRequest->request) && is_object($httpRequest->request) && method_exists($httpRequest->request, 'all')) {
+ /** @var array $request */
+ $request = $httpRequest->request->all();
+ if ($request !== []) {
+ return $request;
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * @param array $data
+ */
+ public function sendData($data): CompletePurchaseResponse
+ {
+ $payload = CallbackPayload::fromArray($data);
+
+ $service = new CallbackService(new CallbackVerifier(
+ new SignatureService($this->getSecretKey()),
+ new ClientOptions(preventCallbackReplay: false)
+ ));
+
+ $referenceId = $this->getReferenceNumber();
+ $context = null;
+
+ $transactionUuid = $this->getTransactionUuid();
+ $productCode = $this->getProductCode() !== '' ? $this->getProductCode() : $this->getMerchantCode();
+ $amount = $this->getAmount();
+ $totalAmount = $this->getTotalAmount() !== '' ? $this->getTotalAmount() : ($amount ?? '');
+
+ if ($transactionUuid !== '' && $productCode !== '' && $totalAmount !== '') {
+ $context = new VerificationExpectation(
+ totalAmount: $totalAmount,
+ transactionUuid: $transactionUuid,
+ productCode: $productCode,
+ referenceId: $referenceId !== '' ? $referenceId : null,
+ );
+ }
+
+ $result = $service->verifyCallback($payload, $context);
+
+ return $this->response = new CompletePurchaseResponse($this, [
+ 'status' => $result->status->value,
+ 'valid' => $result->valid,
+ 'reference_id' => $result->referenceId?->value(),
+ 'message' => $result->message,
+ 'raw' => $result->raw,
+ ]);
+ }
+}
diff --git a/src/Omnipay/Message/CompletePurchaseResponse.php b/src/Omnipay/Message/CompletePurchaseResponse.php
new file mode 100644
index 0000000..c3e1655
--- /dev/null
+++ b/src/Omnipay/Message/CompletePurchaseResponse.php
@@ -0,0 +1,37 @@
+ $data
+ */
+ public function __construct(RequestInterface $request, array $data)
+ {
+ $this->request = $request;
+ $this->data = $data;
+ }
+
+ public function isSuccessful(): bool
+ {
+ return (bool) ($this->data['valid'] ?? false) && (string) ($this->data['status'] ?? '') === 'COMPLETE';
+ }
+
+ public function getMessage(): string
+ {
+ return (string) ($this->data['message'] ?? '');
+ }
+
+ public function getTransactionReference(): ?string
+ {
+ $referenceId = $this->data['reference_id'] ?? null;
+
+ return is_string($referenceId) && $referenceId !== '' ? $referenceId : null;
+ }
+}
diff --git a/src/Omnipay/Message/PurchaseRequest.php b/src/Omnipay/Message/PurchaseRequest.php
new file mode 100644
index 0000000..bfb16d7
--- /dev/null
+++ b/src/Omnipay/Message/PurchaseRequest.php
@@ -0,0 +1,63 @@
+
+ */
+ public function getData(): array
+ {
+ if ($this->getProductCode() === '') {
+ $this->setProductCode($this->getMerchantCode());
+ }
+
+ $this->validate('merchantCode', 'secretKey', 'amount', 'productCode', 'returnUrl', 'failureUrl');
+
+ $transactionUuid = $this->getTransactionUuid();
+ if ($transactionUuid === '') {
+ $transactionUuid = $this->getTransactionId();
+ }
+
+ if ($transactionUuid === '') {
+ throw new \InvalidArgumentException('transactionUuid or transactionId is required.');
+ }
+
+ return [
+ 'transaction_uuid' => $transactionUuid,
+ ];
+ }
+
+ /**
+ * @param array $data
+ */
+ public function sendData($data): PurchaseResponse
+ {
+ $service = new CheckoutService(
+ $this->gatewayConfig(),
+ new EndpointResolver(),
+ new SignatureService($this->getSecretKey()),
+ );
+
+ $intent = $service->createIntent(new CoreCheckoutRequest(
+ amount: (string) $this->getAmount(),
+ taxAmount: $this->getTaxAmount() !== '' ? $this->getTaxAmount() : '0',
+ serviceCharge: $this->getServiceCharge() !== '' ? $this->getServiceCharge() : '0',
+ deliveryCharge: $this->getDeliveryCharge() !== '' ? $this->getDeliveryCharge() : '0',
+ transactionUuid: (string) $data['transaction_uuid'],
+ productCode: $this->getProductCode(),
+ successUrl: (string) $this->getReturnUrl(),
+ failureUrl: $this->getFailureUrl(),
+ ));
+
+ return $this->response = new PurchaseResponse($this, $intent->fields(), $intent->actionUrl);
+ }
+}
diff --git a/src/Omnipay/Message/PurchaseResponse.php b/src/Omnipay/Message/PurchaseResponse.php
new file mode 100644
index 0000000..ead64a1
--- /dev/null
+++ b/src/Omnipay/Message/PurchaseResponse.php
@@ -0,0 +1,49 @@
+ $data
+ */
+ public function __construct(RequestInterface $request, array $data, private readonly string $redirectUrl)
+ {
+ $this->request = $request;
+ $this->data = $data;
+ }
+
+ public function isSuccessful(): bool
+ {
+ return false;
+ }
+
+ public function isRedirect(): bool
+ {
+ return true;
+ }
+
+ public function getRedirectUrl(): string
+ {
+ return $this->redirectUrl;
+ }
+
+ public function getRedirectMethod(): string
+ {
+ return 'POST';
+ }
+
+ /**
+ * @return array
+ */
+ public function getRedirectData(): array
+ {
+ return $this->getData();
+ }
+}
diff --git a/src/Omnipay/Message/VerifyPaymentRequest.php b/src/Omnipay/Message/VerifyPaymentRequest.php
new file mode 100644
index 0000000..6879cb1
--- /dev/null
+++ b/src/Omnipay/Message/VerifyPaymentRequest.php
@@ -0,0 +1,64 @@
+
+ */
+ public function getData(): array
+ {
+ if ($this->getProductCode() === '') {
+ $this->setProductCode($this->getMerchantCode());
+ }
+
+ $this->validate('merchantCode', 'amount', 'productCode');
+
+ $transactionUuid = $this->getTransactionUuid();
+ if ($transactionUuid === '') {
+ $transactionUuid = $this->getTransactionId();
+ }
+
+ if ($transactionUuid === '') {
+ throw new \InvalidArgumentException('transactionUuid or transactionId is required.');
+ }
+
+ return [
+ 'transaction_uuid' => $transactionUuid,
+ 'total_amount' => $this->getTotalAmount() !== '' ? $this->getTotalAmount() : (string) $this->getAmount(),
+ 'product_code' => $this->getProductCode(),
+ ];
+ }
+
+ /**
+ * @param array $data
+ */
+ public function sendData($data): VerifyPaymentResponse
+ {
+ $service = new TransactionService(
+ $this->gatewayConfig(),
+ new EndpointResolver(),
+ $this->transport(),
+ new ClientOptions(),
+ );
+
+ $result = $service->fetchStatus(new CoreTransactionStatusRequest(
+ transactionUuid: (string) $data['transaction_uuid'],
+ totalAmount: (string) $data['total_amount'],
+ productCode: (string) $data['product_code'],
+ ));
+
+ return $this->response = new VerifyPaymentResponse($this, [
+ 'status' => $result->status->value,
+ 'ref_id' => $result->referenceId?->value(),
+ ]);
+ }
+}
diff --git a/src/Omnipay/Message/VerifyPaymentResponse.php b/src/Omnipay/Message/VerifyPaymentResponse.php
new file mode 100644
index 0000000..23e0e35
--- /dev/null
+++ b/src/Omnipay/Message/VerifyPaymentResponse.php
@@ -0,0 +1,37 @@
+ $data
+ */
+ public function __construct(RequestInterface $request, array $data)
+ {
+ $this->request = $request;
+ $this->data = $data;
+ }
+
+ public function getResponseText(): string
+ {
+ return (string) ($this->data['status'] ?? '');
+ }
+
+ public function getReferenceId(): ?string
+ {
+ $referenceId = $this->data['ref_id'] ?? null;
+
+ return is_string($referenceId) && $referenceId !== '' ? $referenceId : null;
+ }
+
+ public function isSuccessful(): bool
+ {
+ return $this->getResponseText() === 'COMPLETE';
+ }
+}
diff --git a/src/Omnipay/SecureGateway.php b/src/Omnipay/SecureGateway.php
new file mode 100644
index 0000000..19d07f4
--- /dev/null
+++ b/src/Omnipay/SecureGateway.php
@@ -0,0 +1,168 @@
+ '',
+ 'secretKey' => '',
+ 'productCode' => '',
+ 'taxAmount' => '0',
+ 'serviceCharge' => '0',
+ 'deliveryCharge' => '0',
+ 'testMode' => false,
+ 'transactionUuid' => '',
+ 'failureUrl' => '',
+ 'timeoutSeconds' => 30,
+ 'transport' => null,
+ ];
+ }
+
+ public function getMerchantCode(): string
+ {
+ return (string) $this->getParameter('merchantCode');
+ }
+
+ public function setMerchantCode(string $value): self
+ {
+ return $this->setParameter('merchantCode', $value);
+ }
+
+ public function getSecretKey(): string
+ {
+ return (string) $this->getParameter('secretKey');
+ }
+
+ public function setSecretKey(string $value): self
+ {
+ return $this->setParameter('secretKey', $value);
+ }
+
+ public function getTaxAmount(): string
+ {
+ return (string) $this->getParameter('taxAmount');
+ }
+
+ public function setTaxAmount(string $value): self
+ {
+ return $this->setParameter('taxAmount', $value);
+ }
+
+ public function getServiceCharge(): string
+ {
+ return (string) $this->getParameter('serviceCharge');
+ }
+
+ public function setServiceCharge(string $value): self
+ {
+ return $this->setParameter('serviceCharge', $value);
+ }
+
+ public function getDeliveryCharge(): string
+ {
+ return (string) $this->getParameter('deliveryCharge');
+ }
+
+ public function setDeliveryCharge(string $value): self
+ {
+ return $this->setParameter('deliveryCharge', $value);
+ }
+
+ public function getTransactionUuid(): string
+ {
+ return (string) $this->getParameter('transactionUuid');
+ }
+
+ public function setTransactionUuid(string $value): self
+ {
+ return $this->setParameter('transactionUuid', $value);
+ }
+
+ public function getProductCode(): string
+ {
+ return (string) $this->getParameter('productCode');
+ }
+
+ public function setProductCode(string $value): self
+ {
+ return $this->setParameter('productCode', $value);
+ }
+
+ public function getFailureUrl(): string
+ {
+ return (string) $this->getParameter('failureUrl');
+ }
+
+ public function getFailedUrl(): string
+ {
+ return $this->getFailureUrl();
+ }
+
+ public function setFailureUrl(string $value): self
+ {
+ return $this->setParameter('failureUrl', $value);
+ }
+
+ public function setFailedUrl(string $value): self
+ {
+ return $this->setFailureUrl($value);
+ }
+
+ public function getReferenceNumber(): string
+ {
+ return (string) $this->getParameter('referenceNumber');
+ }
+
+ public function setReferenceNumber(string $value): self
+ {
+ return $this->setParameter('referenceNumber', $value);
+ }
+
+ /**
+ * @param array $parameters
+ */
+ public function completePurchase(array $parameters = []): \Omnipay\Esewa\Message\CompletePurchaseRequest
+ {
+ $request = $this->createRequest(\Omnipay\Esewa\Message\CompletePurchaseRequest::class, $parameters);
+
+ \assert($request instanceof \Omnipay\Esewa\Message\CompletePurchaseRequest);
+
+ return $request;
+ }
+
+ /**
+ * @param array $parameters
+ */
+ public function purchase(array $parameters = []): \Omnipay\Esewa\Message\PurchaseRequest
+ {
+ $request = $this->createRequest(\Omnipay\Esewa\Message\PurchaseRequest::class, $parameters);
+
+ \assert($request instanceof \Omnipay\Esewa\Message\PurchaseRequest);
+
+ return $request;
+ }
+
+ /**
+ * @param array $parameters
+ */
+ public function verifyPayment(array $parameters = []): \Omnipay\Esewa\Message\VerifyPaymentRequest
+ {
+ $request = $this->createRequest(\Omnipay\Esewa\Message\VerifyPaymentRequest::class, $parameters);
+
+ \assert($request instanceof \Omnipay\Esewa\Message\VerifyPaymentRequest);
+
+ return $request;
+ }
+}
diff --git a/src/Service/CallbackVerifier.php b/src/Service/CallbackVerifier.php
index fc00f60..8245e4e 100644
--- a/src/Service/CallbackVerifier.php
+++ b/src/Service/CallbackVerifier.php
@@ -2,13 +2,14 @@
declare(strict_types=1);
-namespace EsewaPayment\Service;
+namespace Sujip\Esewa\Service;
-use EsewaPayment\Config\ClientOptions;
-use EsewaPayment\Domain\Verification\CallbackPayload;
-use EsewaPayment\Domain\Verification\CallbackVerification;
-use EsewaPayment\Domain\Verification\VerificationExpectation;
-use EsewaPayment\Exception\FraudValidationException;
+use Sujip\Esewa\Config\ClientOptions;
+use Sujip\Esewa\Domain\Verification\CallbackPayload;
+use Sujip\Esewa\Domain\Verification\CallbackVerification;
+use Sujip\Esewa\Domain\Verification\VerificationExpectation;
+use Sujip\Esewa\Domain\Verification\VerificationState;
+use Sujip\Esewa\Exception\FraudValidationException;
final class CallbackVerifier
{
@@ -25,19 +26,15 @@ public function verify(CallbackPayload $payload, ?VerificationExpectation $conte
$validSignature = $this->signatures->verify(
$payload->signature,
- $data->totalAmount,
- $data->transactionUuid,
- $data->productCode,
+ $data->totalAmount->value(),
+ $data->transactionUuid->value(),
+ $data->productCode->value(),
$data->signedFieldNames
);
if (!$validSignature) {
- $this->options->logger->warning('eSewa callback rejected due to invalid signature.', [
- 'event' => 'esewa.callback.invalid_signature',
- 'transaction_uuid' => $data->transactionUuid,
- ]);
-
return new CallbackVerification(
+ VerificationState::INVALID_SIGNATURE,
false,
$data->status,
$data->transactionCode,
@@ -47,20 +44,21 @@ public function verify(CallbackPayload $payload, ?VerificationExpectation $conte
}
if ($context !== null) {
- $this->assertConsistent($context, $data->totalAmount, $data->transactionUuid, $data->productCode, $data->transactionCode);
+ $this->assertConsistent(
+ $context,
+ $data->totalAmount->value(),
+ $data->transactionUuid->value(),
+ $data->productCode->value(),
+ $data->transactionCode?->value()
+ );
}
if ($this->options->preventCallbackReplay) {
$idempotencyKey = $this->resolveIdempotencyKey($payload, $data->transactionCode);
if ($this->options->idempotencyStore->has($idempotencyKey)) {
- $this->options->logger->warning('eSewa callback replay detected.', [
- 'event' => 'esewa.callback.replay_detected',
- 'transaction_uuid' => $data->transactionUuid,
- 'reference_id' => $data->transactionCode,
- ]);
-
return new CallbackVerification(
+ VerificationState::REPLAYED,
false,
$data->status,
$data->transactionCode,
@@ -72,14 +70,14 @@ public function verify(CallbackPayload $payload, ?VerificationExpectation $conte
$this->options->idempotencyStore->put($idempotencyKey);
}
- $this->options->logger->info('eSewa callback verified.', [
- 'event' => 'esewa.callback.verified',
- 'transaction_uuid' => $data->transactionUuid,
- 'status' => $data->status->value,
- 'reference_id' => $data->transactionCode,
- ]);
-
- return new CallbackVerification(true, $data->status, $data->transactionCode, 'Callback verified.', $data->raw);
+ return new CallbackVerification(
+ VerificationState::VERIFIED,
+ true,
+ $data->status,
+ $data->transactionCode,
+ 'Callback verified.',
+ $data->raw
+ );
}
private function assertConsistent(
@@ -89,33 +87,27 @@ private function assertConsistent(
string $productCode,
?string $referenceId
): void {
- if ($context->totalAmount !== $totalAmount) {
+ if ($context->totalAmount->value() !== $totalAmount) {
throw new FraudValidationException('total_amount mismatch during callback verification.');
}
- if ($context->transactionUuid !== $transactionUuid) {
+ if ($context->transactionUuid->value() !== $transactionUuid) {
throw new FraudValidationException('transaction_uuid mismatch during callback verification.');
}
- if ($context->productCode !== $productCode) {
+ if ($context->productCode->value() !== $productCode) {
throw new FraudValidationException('product_code mismatch during callback verification.');
}
- if ($context->referenceId !== null && $context->referenceId !== $referenceId) {
- $this->options->logger->warning('eSewa callback fraud validation failed.', [
- 'event' => 'esewa.callback.fraud_reference_mismatch',
- 'expected_reference_id' => $context->referenceId,
- 'actual_reference_id' => $referenceId,
- ]);
-
+ if ($context->referenceId !== null && $context->referenceId->value() !== $referenceId) {
throw new FraudValidationException('reference_id mismatch during callback verification.');
}
}
- private function resolveIdempotencyKey(CallbackPayload $payload, ?string $referenceId): string
+ private function resolveIdempotencyKey(CallbackPayload $payload, ?\Sujip\Esewa\ValueObject\ReferenceId $referenceId): string
{
- if ($referenceId !== null && $referenceId !== '') {
- return 'ref:'.$referenceId;
+ if ($referenceId !== null) {
+ return 'ref:'.$referenceId->value();
}
return 'digest:'.hash('sha256', $payload->data.'|'.$payload->signature);
diff --git a/src/Service/SignatureService.php b/src/Service/SignatureService.php
index a86eafa..c00eb79 100644
--- a/src/Service/SignatureService.php
+++ b/src/Service/SignatureService.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace EsewaPayment\Service;
+namespace Sujip\Esewa\Service;
final class SignatureService
{
diff --git a/src/Support/FixedDelayRetryPolicy.php b/src/Support/FixedDelayRetryPolicy.php
new file mode 100644
index 0000000..b96aa9f
--- /dev/null
+++ b/src/Support/FixedDelayRetryPolicy.php
@@ -0,0 +1,34 @@
+maxRetries < 0) {
+ throw new \InvalidArgumentException('maxRetries cannot be negative.');
+ }
+
+ if ($this->delayUs < 0) {
+ throw new \InvalidArgumentException('delayUs cannot be negative.');
+ }
+ }
+
+ public function shouldRetry(int $attempt, TransportException $exception): bool
+ {
+ return $attempt < $this->maxRetries;
+ }
+
+ public function delayUs(int $attempt, TransportException $exception): int
+ {
+ return $this->delayUs;
+ }
+}
diff --git a/src/Support/SystemClock.php b/src/Support/SystemClock.php
new file mode 100644
index 0000000..ca561c5
--- /dev/null
+++ b/src/Support/SystemClock.php
@@ -0,0 +1,15 @@
+value;
+ }
+
+ public function add(self $other): self
+ {
+ return self::fromFloat((float) $this->value + (float) $other->value);
+ }
+
+ public function equals(self $other): bool
+ {
+ return $this->value === $other->value;
+ }
+
+ public function __toString(): string
+ {
+ return $this->value;
+ }
+}
diff --git a/src/ValueObject/ProductCode.php b/src/ValueObject/ProductCode.php
new file mode 100644
index 0000000..424f7e5
--- /dev/null
+++ b/src/ValueObject/ProductCode.php
@@ -0,0 +1,38 @@
+value;
+ }
+
+ public function equals(self $other): bool
+ {
+ return $this->value === $other->value;
+ }
+
+ public function __toString(): string
+ {
+ return $this->value;
+ }
+}
diff --git a/src/ValueObject/ReferenceId.php b/src/ValueObject/ReferenceId.php
new file mode 100644
index 0000000..67a760b
--- /dev/null
+++ b/src/ValueObject/ReferenceId.php
@@ -0,0 +1,38 @@
+value;
+ }
+
+ public function equals(self $other): bool
+ {
+ return $this->value === $other->value;
+ }
+
+ public function __toString(): string
+ {
+ return $this->value;
+ }
+}
diff --git a/src/ValueObject/TransactionUuid.php b/src/ValueObject/TransactionUuid.php
new file mode 100644
index 0000000..be70068
--- /dev/null
+++ b/src/ValueObject/TransactionUuid.php
@@ -0,0 +1,38 @@
+value;
+ }
+
+ public function equals(self $other): bool
+ {
+ return $this->value === $other->value;
+ }
+
+ public function __toString(): string
+ {
+ return $this->value;
+ }
+}
diff --git a/tests/Fakes/ArrayLogger.php b/tests/Fakes/ArrayLogger.php
deleted file mode 100644
index f6657d7..0000000
--- a/tests/Fakes/ArrayLogger.php
+++ /dev/null
@@ -1,108 +0,0 @@
-}> */
- public array $records = [];
-
- /**
- * @param string|\Stringable $message
- * @param array $context
- */
- public function emergency($message, array $context = []): void
- {
- $this->log(LogLevel::EMERGENCY, (string) $message, $context);
- }
-
- /**
- * @param string|\Stringable $message
- * @param array $context
- */
- public function alert($message, array $context = []): void
- {
- $this->log(LogLevel::ALERT, (string) $message, $context);
- }
-
- /**
- * @param string|\Stringable $message
- * @param array $context
- */
- public function critical($message, array $context = []): void
- {
- $this->log(LogLevel::CRITICAL, (string) $message, $context);
- }
-
- /**
- * @param string|\Stringable $message
- * @param array $context
- */
- public function error($message, array $context = []): void
- {
- $this->log(LogLevel::ERROR, (string) $message, $context);
- }
-
- /**
- * @param string|\Stringable $message
- * @param array $context
- */
- public function warning($message, array $context = []): void
- {
- $this->log(LogLevel::WARNING, (string) $message, $context);
- }
-
- /**
- * @param string|\Stringable $message
- * @param array $context
- */
- public function notice($message, array $context = []): void
- {
- $this->log(LogLevel::NOTICE, (string) $message, $context);
- }
-
- /**
- * @param string|\Stringable $message
- * @param array $context
- */
- public function info($message, array $context = []): void
- {
- $this->log(LogLevel::INFO, (string) $message, $context);
- }
-
- /**
- * @param string|\Stringable $message
- * @param array $context
- */
- public function debug($message, array $context = []): void
- {
- $this->log(LogLevel::DEBUG, (string) $message, $context);
- }
-
- /**
- * @param string $level
- * @param string|\Stringable $message
- * @param array $context
- */
- public function log($level, $message, array $context = []): void
- {
- $normalizedContext = [];
-
- foreach ($context as $key => $value) {
- if (is_string($key)) {
- $normalizedContext[$key] = $value;
- }
- }
-
- $this->records[] = [
- 'level' => (string) $level,
- 'message' => (string) $message,
- 'context' => $normalizedContext,
- ];
- }
-}
diff --git a/tests/Fakes/FakeTransport.php b/tests/Fakes/FakeTransport.php
index 47a0233..8806206 100644
--- a/tests/Fakes/FakeTransport.php
+++ b/tests/Fakes/FakeTransport.php
@@ -2,9 +2,9 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Fakes;
+namespace Sujip\Esewa\Tests\Fakes;
-use EsewaPayment\Contracts\TransportInterface;
+use Sujip\Esewa\Contracts\TransportInterface;
final class FakeTransport implements TransportInterface
{
diff --git a/tests/Fakes/FlakyTransport.php b/tests/Fakes/FlakyTransport.php
index f280dda..cf49112 100644
--- a/tests/Fakes/FlakyTransport.php
+++ b/tests/Fakes/FlakyTransport.php
@@ -2,9 +2,9 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Fakes;
+namespace Sujip\Esewa\Tests\Fakes;
-use EsewaPayment\Contracts\TransportInterface;
+use Sujip\Esewa\Contracts\TransportInterface;
final class FlakyTransport implements TransportInterface
{
diff --git a/tests/Fakes/FrozenClock.php b/tests/Fakes/FrozenClock.php
new file mode 100644
index 0000000..fc41368
--- /dev/null
+++ b/tests/Fakes/FrozenClock.php
@@ -0,0 +1,24 @@
+now;
+ }
+
+ public function advanceSeconds(int $seconds): void
+ {
+ $this->now = $this->now->modify(sprintf('+%d seconds', $seconds));
+ }
+}
diff --git a/tests/Stubs/OmnipayCommon.php b/tests/Stubs/OmnipayCommon.php
new file mode 100644
index 0000000..f07d469
--- /dev/null
+++ b/tests/Stubs/OmnipayCommon.php
@@ -0,0 +1,234 @@
+
+ */
+ public function getData(): array;
+
+ public function send(): ResponseInterface;
+}
+
+interface ResponseInterface
+{
+ public function isSuccessful(): bool;
+
+ public function getData(): mixed;
+}
+
+interface RedirectResponseInterface extends ResponseInterface
+{
+ public function isRedirect(): bool;
+
+ public function getRedirectUrl(): string;
+
+ public function getRedirectMethod(): string;
+
+ /**
+ * @return array
+ */
+ public function getRedirectData(): array;
+}
+
+abstract class AbstractResponse implements ResponseInterface
+{
+ protected RequestInterface $request;
+
+ /**
+ * @var mixed
+ */
+ protected $data;
+
+ public function getRequest(): RequestInterface
+ {
+ return $this->request;
+ }
+
+ public function getData(): mixed
+ {
+ return $this->data;
+ }
+}
+
+abstract class AbstractRequest implements RequestInterface
+{
+ /**
+ * @var array
+ */
+ protected array $parameters = [];
+
+ protected object $httpRequest;
+
+ protected ?ResponseInterface $response = null;
+
+ /**
+ * @param mixed $httpRequest
+ */
+ public function __construct(mixed $httpClient = null, mixed $httpRequest = null)
+ {
+ unset($httpClient);
+
+ $this->httpRequest = $httpRequest ?? new class {
+ public object $query;
+ public object $request;
+
+ public function __construct()
+ {
+ $this->query = new class {
+ /**
+ * @return array
+ */
+ public function all(): array
+ {
+ return [];
+ }
+ };
+ $this->request = new class {
+ /**
+ * @return array
+ */
+ public function all(): array
+ {
+ return [];
+ }
+ };
+ }
+ };
+ }
+
+ /**
+ * @param array $parameters
+ */
+ public function initialize(array $parameters = []): static
+ {
+ foreach ($parameters as $key => $value) {
+ $this->setParameter((string) $key, $value);
+ }
+
+ return $this;
+ }
+
+ public function getParameter(string $key): mixed
+ {
+ return $this->parameters[$key] ?? null;
+ }
+
+ public function setParameter(string $key, mixed $value): static
+ {
+ $this->parameters[$key] = $value;
+
+ return $this;
+ }
+
+ public function getAmount(): ?string
+ {
+ $amount = $this->getParameter('amount');
+
+ return $amount === null ? null : (string) $amount;
+ }
+
+ public function getReturnUrl(): ?string
+ {
+ $returnUrl = $this->getParameter('returnUrl');
+
+ return $returnUrl === null ? null : (string) $returnUrl;
+ }
+
+ public function setReturnUrl(string $value): static
+ {
+ return $this->setParameter('returnUrl', $value);
+ }
+
+ public function getTransactionId(): ?string
+ {
+ $transactionId = $this->getParameter('transactionId');
+
+ return $transactionId === null ? null : (string) $transactionId;
+ }
+
+ public function getTestMode(): bool
+ {
+ return (bool) $this->getParameter('testMode');
+ }
+
+ public function send(): ResponseInterface
+ {
+ return $this->sendData($this->getData());
+ }
+
+ protected function validate(string ...$required): void
+ {
+ foreach ($required as $field) {
+ $value = $this->getParameter($field);
+ if ($value === null || $value === '') {
+ throw new \InvalidArgumentException(sprintf('The %s parameter is required.', $field));
+ }
+ }
+ }
+
+ abstract public function sendData(mixed $data): ResponseInterface;
+}
+
+namespace Omnipay\Common;
+
+abstract class AbstractGateway
+{
+ /**
+ * @var array
+ */
+ private array $parameters = [];
+
+ /**
+ * @return array
+ */
+ abstract public function getDefaultParameters(): array;
+
+ /**
+ * @param class-string<\Omnipay\Common\Message\AbstractRequest> $class
+ * @param array $parameters
+ */
+ protected function createRequest(string $class, array $parameters = []): \Omnipay\Common\Message\AbstractRequest
+ {
+ $request = new $class();
+
+ if (!$request instanceof \Omnipay\Common\Message\AbstractRequest) {
+ throw new \InvalidArgumentException('Invalid Omnipay request class.');
+ }
+
+ $request->initialize(array_replace($this->getDefaultParameters(), $this->parameters, $parameters));
+
+ return $request;
+ }
+
+ protected function getParameter(string $key): mixed
+ {
+ return $this->parameters[$key] ?? $this->getDefaultParameters()[$key] ?? null;
+ }
+
+ protected function setParameter(string $key, mixed $value): static
+ {
+ $this->parameters[$key] = $value;
+
+ return $this;
+ }
+
+ public function setTestMode(bool $value): static
+ {
+ return $this->setParameter('testMode', $value);
+ }
+
+ public function getTestMode(): bool
+ {
+ return (bool) $this->getParameter('testMode');
+ }
+
+ public function setReturnUrl(string $value): static
+ {
+ return $this->setParameter('returnUrl', $value);
+ }
+}
diff --git a/tests/Unit/CallbackPayloadTest.php b/tests/Unit/CallbackPayloadTest.php
index 48dbdf7..2147e5f 100644
--- a/tests/Unit/CallbackPayloadTest.php
+++ b/tests/Unit/CallbackPayloadTest.php
@@ -2,11 +2,11 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Unit;
+namespace Sujip\Esewa\Tests\Unit;
-use EsewaPayment\Domain\Transaction\PaymentStatus;
-use EsewaPayment\Domain\Verification\CallbackPayload;
-use EsewaPayment\Exception\InvalidPayloadException;
+use Sujip\Esewa\Domain\Transaction\PaymentStatus;
+use Sujip\Esewa\Domain\Verification\CallbackPayload;
+use Sujip\Esewa\Exception\InvalidPayloadException;
use PHPUnit\Framework\TestCase;
final class CallbackPayloadTest extends TestCase
diff --git a/tests/Unit/CallbackServiceTest.php b/tests/Unit/CallbackServiceTest.php
index 69970b8..f58aeea 100644
--- a/tests/Unit/CallbackServiceTest.php
+++ b/tests/Unit/CallbackServiceTest.php
@@ -2,16 +2,17 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Unit;
-
-use EsewaPayment\Client\EsewaClient;
-use EsewaPayment\Config\GatewayConfig;
-use EsewaPayment\Domain\Transaction\PaymentStatus;
-use EsewaPayment\Domain\Verification\CallbackPayload;
-use EsewaPayment\Domain\Verification\VerificationExpectation;
-use EsewaPayment\Exception\FraudValidationException;
-use EsewaPayment\Service\SignatureService;
-use EsewaPayment\Tests\Fakes\FakeTransport;
+namespace Sujip\Esewa\Tests\Unit;
+
+use Sujip\Esewa\Client\EsewaClient;
+use Sujip\Esewa\Config\GatewayConfig;
+use Sujip\Esewa\Domain\Transaction\PaymentStatus;
+use Sujip\Esewa\Domain\Verification\CallbackPayload;
+use Sujip\Esewa\Domain\Verification\VerificationExpectation;
+use Sujip\Esewa\Domain\Verification\VerificationState;
+use Sujip\Esewa\Exception\FraudValidationException;
+use Sujip\Esewa\Service\SignatureService;
+use Sujip\Esewa\Tests\Fakes\FakeTransport;
use PHPUnit\Framework\TestCase;
final class CallbackServiceTest extends TestCase
@@ -52,6 +53,7 @@ public function testVerifyValidCallbackPayload(): void
));
$this->assertTrue($result->valid);
+ $this->assertSame(VerificationState::VERIFIED, $result->state);
$this->assertTrue($result->isSuccessful());
}
@@ -76,6 +78,7 @@ public function testVerifyReturnsInvalidResultWhenSignatureIsWrong(): void
$result = $gateway->callbacks()->verifyCallback($payload);
$this->assertFalse($result->valid);
+ $this->assertSame(VerificationState::INVALID_SIGNATURE, $result->state);
$this->assertSame(PaymentStatus::COMPLETE, $result->status);
$this->assertSame('Invalid callback signature.', $result->message);
$this->assertFalse($result->isSuccessful());
diff --git a/tests/Unit/CheckoutServiceTest.php b/tests/Unit/CheckoutServiceTest.php
index 6099173..c0a7bf9 100644
--- a/tests/Unit/CheckoutServiceTest.php
+++ b/tests/Unit/CheckoutServiceTest.php
@@ -2,12 +2,13 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Unit;
+namespace Sujip\Esewa\Tests\Unit;
-use EsewaPayment\Client\EsewaClient;
-use EsewaPayment\Config\GatewayConfig;
-use EsewaPayment\Domain\Checkout\CheckoutRequest;
-use EsewaPayment\Tests\Fakes\FakeTransport;
+use Sujip\Esewa\Client\EsewaClient;
+use Sujip\Esewa\Esewa;
+use Sujip\Esewa\Config\GatewayConfig;
+use Sujip\Esewa\Domain\Checkout\CheckoutRequest;
+use Sujip\Esewa\Tests\Fakes\FakeTransport;
use PHPUnit\Framework\TestCase;
final class CheckoutServiceTest extends TestCase
@@ -41,4 +42,26 @@ public function testCreateIntentBuildsFormFields(): void
$this->assertSame('TXN-1001', $form['fields']['transaction_uuid']);
$this->assertNotSame('', $form['fields']['signature']);
}
+
+ public function testFactoryCanBootstrapWithoutExplicitTransportForCheckout(): void
+ {
+ $gateway = Esewa::make(
+ merchantCode: 'EPAYTEST',
+ secretKey: 'secret',
+ environment: 'uat',
+ );
+
+ $intent = $gateway->checkout()->createIntent(new CheckoutRequest(
+ amount: '100',
+ taxAmount: '0',
+ serviceCharge: '0',
+ deliveryCharge: '0',
+ transactionUuid: 'TXN-1002',
+ productCode: 'EPAYTEST',
+ successUrl: 'https://merchant.test/success',
+ failureUrl: 'https://merchant.test/failure',
+ ));
+
+ $this->assertSame('TXN-1002', $intent->fields()['transaction_uuid']);
+ }
}
diff --git a/tests/Unit/DomainValidationTest.php b/tests/Unit/DomainValidationTest.php
index 082715f..9908cb7 100644
--- a/tests/Unit/DomainValidationTest.php
+++ b/tests/Unit/DomainValidationTest.php
@@ -2,10 +2,10 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Unit;
+namespace Sujip\Esewa\Tests\Unit;
-use EsewaPayment\Domain\Checkout\CheckoutRequest;
-use EsewaPayment\Domain\Transaction\TransactionStatusRequest;
+use Sujip\Esewa\Domain\Checkout\CheckoutRequest;
+use Sujip\Esewa\Domain\Transaction\TransactionStatusRequest;
use PHPUnit\Framework\TestCase;
final class DomainValidationTest extends TestCase
@@ -46,7 +46,7 @@ public function testCheckoutRequestThrowsWhenRequiredFieldMissing(): void
public function testTransactionStatusRequestThrowsWhenRequiredFieldMissing(): void
{
$this->expectException(\InvalidArgumentException::class);
- $this->expectExceptionMessage('transactionUuid, totalAmount and productCode are required.');
+ $this->expectExceptionMessage('transactionUuid is required.');
new TransactionStatusRequest(
transactionUuid: '',
diff --git a/tests/Unit/EndpointResolverTest.php b/tests/Unit/EndpointResolverTest.php
index 4032c42..d698374 100644
--- a/tests/Unit/EndpointResolverTest.php
+++ b/tests/Unit/EndpointResolverTest.php
@@ -2,10 +2,10 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Unit;
+namespace Sujip\Esewa\Tests\Unit;
-use EsewaPayment\Config\EndpointResolver;
-use EsewaPayment\Config\GatewayConfig;
+use Sujip\Esewa\Config\EndpointResolver;
+use Sujip\Esewa\Config\GatewayConfig;
use PHPUnit\Framework\TestCase;
final class EndpointResolverTest extends TestCase
diff --git a/tests/Unit/EsewaPaymentTest.php b/tests/Unit/EsewaTest.php
similarity index 69%
rename from tests/Unit/EsewaPaymentTest.php
rename to tests/Unit/EsewaTest.php
index 98dc063..6cc4957 100644
--- a/tests/Unit/EsewaPaymentTest.php
+++ b/tests/Unit/EsewaTest.php
@@ -2,18 +2,18 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Unit;
+namespace Sujip\Esewa\Tests\Unit;
-use EsewaPayment\Client\EsewaClient;
-use EsewaPayment\EsewaPayment;
-use EsewaPayment\Tests\Fakes\FakeTransport;
+use Sujip\Esewa\Client\EsewaClient;
+use Sujip\Esewa\Esewa;
+use Sujip\Esewa\Tests\Fakes\FakeTransport;
use PHPUnit\Framework\TestCase;
-final class EsewaPaymentTest extends TestCase
+final class EsewaTest extends TestCase
{
public function testMakeCreatesClientWithMinimalSetup(): void
{
- $client = EsewaPayment::make(
+ $client = Esewa::make(
merchantCode: 'EPAYTEST',
secretKey: 'secret',
transport: new FakeTransport([]),
diff --git a/tests/Unit/GatewayConfigTest.php b/tests/Unit/GatewayConfigTest.php
index 03583ef..39d22cf 100644
--- a/tests/Unit/GatewayConfigTest.php
+++ b/tests/Unit/GatewayConfigTest.php
@@ -2,10 +2,10 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Unit;
+namespace Sujip\Esewa\Tests\Unit;
-use EsewaPayment\Config\Environment;
-use EsewaPayment\Config\GatewayConfig;
+use Sujip\Esewa\Config\Environment;
+use Sujip\Esewa\Config\GatewayConfig;
use PHPUnit\Framework\TestCase;
final class GatewayConfigTest extends TestCase
diff --git a/tests/Unit/IdempotencyStoreTest.php b/tests/Unit/IdempotencyStoreTest.php
new file mode 100644
index 0000000..70231f4
--- /dev/null
+++ b/tests/Unit/IdempotencyStoreTest.php
@@ -0,0 +1,47 @@
+put('callback-1');
+
+ $this->assertTrue($store->has('callback-1'));
+
+ $clock->advanceSeconds(11);
+
+ $this->assertFalse($store->has('callback-1'));
+ }
+
+ public function testPdoStoreExpiresKeys(): void
+ {
+ if (!extension_loaded('pdo_sqlite')) {
+ $this->markTestSkipped('pdo_sqlite is not available.');
+ }
+
+ $clock = new FrozenClock(new \DateTimeImmutable('2026-03-13 00:00:00 UTC'));
+ $store = new PdoIdempotencyStore(new PDO('sqlite::memory:'), ttlSeconds: 10, clock: $clock);
+
+ $store->put('callback-1');
+
+ $this->assertTrue($store->has('callback-1'));
+
+ $clock->advanceSeconds(11);
+
+ $this->assertFalse($store->has('callback-1'));
+ }
+}
diff --git a/tests/Unit/NamespaceAliasTest.php b/tests/Unit/NamespaceAliasTest.php
new file mode 100644
index 0000000..bdd3aba
--- /dev/null
+++ b/tests/Unit/NamespaceAliasTest.php
@@ -0,0 +1,34 @@
+checkout()->createIntent(new CheckoutRequest(
+ amount: '100',
+ taxAmount: '0',
+ serviceCharge: '0',
+ deliveryCharge: '0',
+ transactionUuid: 'TXN-ALIAS-1',
+ productCode: 'EPAYTEST',
+ successUrl: 'https://merchant.test/success',
+ failureUrl: 'https://merchant.test/failure',
+ ));
+
+ $this->assertSame('TXN-ALIAS-1', $intent->fields()['transaction_uuid']);
+ }
+}
diff --git a/tests/Unit/OmnipayBridgeTest.php b/tests/Unit/OmnipayBridgeTest.php
new file mode 100644
index 0000000..19f2010
--- /dev/null
+++ b/tests/Unit/OmnipayBridgeTest.php
@@ -0,0 +1,165 @@
+setMerchantCode('EPAYTEST');
+ $gateway->setSecretKey('secret');
+ $gateway->setProductCode('EPAYTEST');
+ $gateway->setTestMode(true);
+ $gateway->setReturnUrl('https://merchant.test/success');
+ $gateway->setFailureUrl('https://merchant.test/failure');
+
+ $response = $gateway->purchase([
+ 'amount' => '100',
+ 'transactionId' => 'TXN-2001',
+ ])->send();
+
+ $this->assertInstanceOf(PurchaseResponse::class, $response);
+ $this->assertFalse($response->isSuccessful());
+ $this->assertTrue($response->isRedirect());
+ $this->assertSame('POST', $response->getRedirectMethod());
+ $this->assertStringContainsString('/v2/form', $response->getRedirectUrl());
+ $this->assertSame('TXN-2001', $response->getRedirectData()['transaction_uuid']);
+ $this->assertSame('100.00', $response->getRedirectData()['total_amount']);
+ }
+
+ public function testVerifyPaymentRequestMapsStatusResponse(): void
+ {
+ $transport = new FakeTransport([
+ 'status' => 'COMPLETE',
+ 'ref_id' => 'REF-2001',
+ ]);
+
+ $gateway = new SecureGateway();
+ $gateway->setMerchantCode('EPAYTEST');
+ $gateway->setSecretKey('secret');
+ $gateway->setProductCode('EPAYTEST');
+ $gateway->setTestMode(true);
+
+ $response = $gateway->verifyPayment([
+ 'amount' => '100.00',
+ 'transactionId' => 'TXN-2001',
+ 'transport' => $transport,
+ ])->send();
+
+ $this->assertInstanceOf(VerifyPaymentResponse::class, $response);
+ $this->assertTrue($response->isSuccessful());
+ $this->assertSame('COMPLETE', $response->getResponseText());
+ $this->assertSame('REF-2001', $response->getReferenceId());
+ $this->assertSame('TXN-2001', $transport->lastQuery['transaction_uuid']);
+ }
+
+ public function testCompletePurchaseRequestVerifiesCallbackPayload(): void
+ {
+ $request = new CompletePurchaseRequest();
+ $request->initialize([
+ 'merchantCode' => 'EPAYTEST',
+ 'secretKey' => 'secret',
+ 'productCode' => 'EPAYTEST',
+ 'transactionUuid' => 'TXN-2001',
+ 'totalAmount' => '100.00',
+ 'referenceNumber' => 'REF-2001',
+ ]);
+
+ $data = [
+ 'status' => 'COMPLETE',
+ 'transaction_uuid' => 'TXN-2001',
+ 'total_amount' => '100.00',
+ 'product_code' => 'EPAYTEST',
+ 'signed_field_names' => 'total_amount,transaction_uuid,product_code',
+ 'transaction_code' => 'REF-2001',
+ ];
+
+ $signature = (new SignatureService('secret'))->generate(
+ '100.00',
+ 'TXN-2001',
+ 'EPAYTEST',
+ 'total_amount,transaction_uuid,product_code'
+ );
+
+ $this->setHttpRequestPayload($request, [
+ 'data' => base64_encode((string) json_encode($data)),
+ 'signature' => $signature,
+ ]);
+
+ $response = $request->send();
+
+ $this->assertInstanceOf(CompletePurchaseResponse::class, $response);
+ $this->assertTrue($response->isSuccessful());
+ $this->assertSame('REF-2001', $response->getTransactionReference());
+ $this->assertSame('COMPLETE', $response->getData()['status']);
+ }
+
+ public function testSecureGatewayCreatesExpectedRequestTypes(): void
+ {
+ $gateway = new SecureGateway();
+
+ $this->assertInstanceOf(PurchaseRequest::class, $gateway->purchase());
+ $this->assertInstanceOf(CompletePurchaseRequest::class, $gateway->completePurchase());
+ $this->assertInstanceOf(VerifyPaymentRequest::class, $gateway->verifyPayment());
+ }
+
+ /**
+ * @param array $payload
+ */
+ private function setHttpRequestPayload(CompletePurchaseRequest $request, array $payload): void
+ {
+ $httpRequest = new class ($payload) {
+ public object $query;
+ public object $request;
+
+ /**
+ * @param array $payload
+ */
+ public function __construct(array $payload)
+ {
+ $this->query = new class {
+ /**
+ * @return array
+ */
+ public function all(): array
+ {
+ return [];
+ }
+ };
+ $this->request = new class ($payload) {
+ /**
+ * @param array $payload
+ */
+ public function __construct(private readonly array $payload)
+ {
+ }
+
+ /**
+ * @return array
+ */
+ public function all(): array
+ {
+ return $this->payload;
+ }
+ };
+ }
+ };
+
+ $reflection = new \ReflectionProperty($request, 'httpRequest');
+ $reflection->setValue($request, $httpRequest);
+ }
+}
diff --git a/tests/Unit/ProductionHardeningTest.php b/tests/Unit/ProductionHardeningTest.php
index 6cf0857..59b030c 100644
--- a/tests/Unit/ProductionHardeningTest.php
+++ b/tests/Unit/ProductionHardeningTest.php
@@ -2,32 +2,31 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Unit;
-
-use EsewaPayment\Client\EsewaClient;
-use EsewaPayment\Config\ClientOptions;
-use EsewaPayment\Config\GatewayConfig;
-use EsewaPayment\Domain\Transaction\PaymentStatus;
-use EsewaPayment\Domain\Transaction\TransactionStatusRequest;
-use EsewaPayment\Domain\Verification\CallbackPayload;
-use EsewaPayment\Exception\TransportException;
-use EsewaPayment\Infrastructure\Idempotency\InMemoryIdempotencyStore;
-use EsewaPayment\Service\SignatureService;
-use EsewaPayment\Tests\Fakes\ArrayLogger;
-use EsewaPayment\Tests\Fakes\FakeTransport;
-use EsewaPayment\Tests\Fakes\FlakyTransport;
+namespace Sujip\Esewa\Tests\Unit;
+
+use Sujip\Esewa\Client\EsewaClient;
+use Sujip\Esewa\Config\ClientOptions;
+use Sujip\Esewa\Config\GatewayConfig;
+use Sujip\Esewa\Domain\Transaction\PaymentStatus;
+use Sujip\Esewa\Domain\Transaction\TransactionStatusRequest;
+use Sujip\Esewa\Domain\Verification\CallbackPayload;
+use Sujip\Esewa\Domain\Verification\VerificationState;
+use Sujip\Esewa\Exception\TransportException;
+use Sujip\Esewa\Infrastructure\Idempotency\InMemoryIdempotencyStore;
+use Sujip\Esewa\Contracts\RetryPolicyInterface;
+use Sujip\Esewa\Service\SignatureService;
+use Sujip\Esewa\Tests\Fakes\FakeTransport;
+use Sujip\Esewa\Tests\Fakes\FlakyTransport;
use PHPUnit\Framework\TestCase;
final class ProductionHardeningTest extends TestCase
{
public function testCallbackReplayProtectionRejectsDuplicatePayload(): void
{
- $logger = new ArrayLogger();
$store = new InMemoryIdempotencyStore();
$options = new ClientOptions(
preventCallbackReplay: true,
idempotencyStore: $store,
- logger: $logger
);
$gateway = new EsewaClient(
@@ -52,14 +51,15 @@ public function testCallbackReplayProtectionRejectsDuplicatePayload(): void
$second = $gateway->callbacks()->verifyCallback($payload);
$this->assertTrue($first->valid);
+ $this->assertSame(VerificationState::VERIFIED, $first->state);
$this->assertFalse($second->valid);
+ $this->assertSame(VerificationState::REPLAYED, $second->state);
+ $this->assertTrue($second->isReplayed());
$this->assertSame('Duplicate callback detected.', $second->message);
- $this->assertTrue($this->hasEvent($logger, 'esewa.callback.replay_detected'));
}
public function testStatusCheckRetriesOnTransportErrorsAndEventuallySucceeds(): void
{
- $logger = new ArrayLogger();
$transport = new FlakyTransport([
new TransportException('temporary timeout'),
new TransportException('temporary 502'),
@@ -69,7 +69,7 @@ public function testStatusCheckRetriesOnTransportErrorsAndEventuallySucceeds():
$gateway = new EsewaClient(
GatewayConfig::make('EPAYTEST', 'secret', 'uat'),
$transport,
- new ClientOptions(maxStatusRetries: 2, statusRetryDelayMs: 0, logger: $logger)
+ new ClientOptions(maxStatusRetries: 2, statusRetryDelayMs: 0)
);
$result = $gateway->transactions()->fetchStatus(new TransactionStatusRequest(
@@ -80,7 +80,6 @@ public function testStatusCheckRetriesOnTransportErrorsAndEventuallySucceeds():
$this->assertSame(3, $transport->attempts);
$this->assertSame(PaymentStatus::COMPLETE, $result->status);
- $this->assertTrue($this->hasEvent($logger, 'esewa.status.retry'));
}
public function testStatusCheckThrowsWhenRetryLimitExceeded(): void
@@ -105,14 +104,51 @@ public function testStatusCheckThrowsWhenRetryLimitExceeded(): void
));
}
- private function hasEvent(ArrayLogger $logger, string $event): bool
+ public function testStatusCheckUsesCustomRetryPolicy(): void
{
- foreach ($logger->records as $record) {
- if (($record['context']['event'] ?? null) === $event) {
- return true;
+ $transport = new FlakyTransport([
+ new TransportException('temporary timeout'),
+ ['status' => 'COMPLETE', 'ref_id' => 'REF-OK'],
+ ]);
+
+ $policy = new class implements RetryPolicyInterface {
+ public int $shouldRetryCalls = 0;
+ public int $delayCalls = 0;
+
+ public function shouldRetry(int $attempt, TransportException $exception): bool
+ {
+ ++$this->shouldRetryCalls;
+
+ return $attempt < 1;
}
- }
- return false;
+ public function delayUs(int $attempt, TransportException $exception): int
+ {
+ ++$this->delayCalls;
+
+ return 0;
+ }
+ };
+
+ $gateway = new EsewaClient(
+ GatewayConfig::make('EPAYTEST', 'secret', 'uat'),
+ $transport,
+ new ClientOptions(
+ maxStatusRetries: 0,
+ statusRetryDelayMs: 0,
+ retryPolicy: $policy,
+ )
+ );
+
+ $result = $gateway->transactions()->fetchStatus(new TransactionStatusRequest(
+ transactionUuid: 'TXN-3003',
+ totalAmount: '500.00',
+ productCode: 'EPAYTEST'
+ ));
+
+ $this->assertSame(1, $policy->shouldRetryCalls);
+ $this->assertSame(1, $policy->delayCalls);
+ $this->assertSame(2, $transport->attempts);
+ $this->assertSame(PaymentStatus::COMPLETE, $result->status);
}
}
diff --git a/tests/Unit/SignatureServiceTest.php b/tests/Unit/SignatureServiceTest.php
index 4189ddb..bd3bd2d 100644
--- a/tests/Unit/SignatureServiceTest.php
+++ b/tests/Unit/SignatureServiceTest.php
@@ -2,9 +2,9 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Unit;
+namespace Sujip\Esewa\Tests\Unit;
-use EsewaPayment\Service\SignatureService;
+use Sujip\Esewa\Service\SignatureService;
use PHPUnit\Framework\TestCase;
final class SignatureServiceTest extends TestCase
diff --git a/tests/Unit/TransactionServiceTest.php b/tests/Unit/TransactionServiceTest.php
index 3942c86..c2d2164 100644
--- a/tests/Unit/TransactionServiceTest.php
+++ b/tests/Unit/TransactionServiceTest.php
@@ -2,13 +2,13 @@
declare(strict_types=1);
-namespace EsewaPayment\Tests\Unit;
+namespace Sujip\Esewa\Tests\Unit;
-use EsewaPayment\Client\EsewaClient;
-use EsewaPayment\Config\GatewayConfig;
-use EsewaPayment\Domain\Transaction\PaymentStatus;
-use EsewaPayment\Domain\Transaction\TransactionStatusRequest;
-use EsewaPayment\Tests\Fakes\FakeTransport;
+use Sujip\Esewa\Client\EsewaClient;
+use Sujip\Esewa\Config\GatewayConfig;
+use Sujip\Esewa\Domain\Transaction\PaymentStatus;
+use Sujip\Esewa\Domain\Transaction\TransactionStatusRequest;
+use Sujip\Esewa\Tests\Fakes\FakeTransport;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..755b98c
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,9 @@
+