Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
279 changes: 212 additions & 67 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Loading
Loading