⚡ v2.1.0 — Now with Fawry

Enterprise Payment
Orchestration Platform

Multi-gateway payments, immutable transaction ledger, webhook reliability, auto-reconciliation, and recovery engine — all in one Laravel package.

Quick Install → View on GitHub
7
Payment Gateways
6
Database Tables
6
Laravel Events
10+
API Endpoints
5x
Webhook Retry Tiers
v2.1
Current Version
Multi-Gateway

7 Payment Gateways — One Interface

Driver-based architecture using the Strategy Pattern. Add new gateways by implementing PaymentDriverInterface — zero changes to core code.

🇰🇼
KNET
Kuwait / Gulf
✓ Full Support
🌍
MyFatoorah
Gulf & MENA
✓ Full Support
💠
Tap Payments
MENA
✓ Full Support
🔷
PayTabs
MENA / Global
✓ Full Support
🟣
Stripe
Global
✓ Full Support
🔵
PayPal
Global
✓ Full Support
🇪🇬
Fawry
Egypt / MENA
✓ Full Support

Core Features

Enterprise-Grade Reliability

Built for high transaction volume, financial traceability, and production environments that can't afford data loss.

📒
Immutable Transaction Ledger
Every payment is recorded in pe_transactions with a ULID and full audit trail in pe_transaction_events. Status changes are append-only — nothing is ever deleted.
ULIDAppend-OnlyMulti-Tenant
🪝
Webhook Reliability Layer
Raw webhook payloads are persisted before processing. Idempotency prevents duplicates. Failed webhooks retry with exponential backoff (1m→5m→15m→1h→24h) before moving to Dead Letter Queue.
IdempotencyDLQ5-Tier Retry
⚖️
Reconciliation Engine
Automated comparison of gateway transactions vs internal records. Detects missing payments, amount mismatches (±0.01 tolerance), status inconsistencies.
Amount ToleranceStatus CheckGap Detection
🔄
Auto-Recovery Engine
Scans for stale pending transactions older than N minutes, queries the gateway directly, and auto-heals inconsistencies. PaymentRecovered event fired on success.
Stale DetectionGateway InquiryAuto-Heal
📥
Historical Backfill Engine
Sync full date ranges or only missing transactions from any gateway. Essential after downtime, system migration, or partial sync failures.
Full SyncGap-FillDate Range
Event-Driven Architecture
6 domain events fire at every lifecycle stage. Listen to PaymentCaptured to update your orders, PaymentRecovered for notifications.
6 EventsDecoupledZero Side Effects
🏢
Multi-Tenant Support
Every transaction and reconciliation report carries a tenant_id. Configure a tenant_resolver callable in config to automatically scope all operations per tenant.
tenant_idScoped QueriesSaaS-Ready
🔒
Security by Design
Sanctum-protected API, Stripe webhook HMAC-SHA256 verification, immutable raw payload storage, no credentials in logs. All PHP files enforce declare(strict_types=1).
SanctumHMAC SigStrict Types
🧩
SOLID Architecture
PaymentDriverInterface, TransactionRepositoryInterface, ReconciliationInterface. Readonly DTOs. Final services. Every dependency is injected and mockable.
Interfacesreadonly DTOsDI

Architecture

Payment Lifecycle Flow

Every payment follows a deterministic, traceable path through the system.

📱
Your AppInitiatePaymentDTO
🎯
PaymentManagerinitiate()
💾
Transaction Ledgerpe_transactions
🌐
Gateway Driverinitiate()
↩️
Redirect URLUser pays
📬
Callback / Webhookraw payload
💾
WebhookProcessorpersist → queue
⚙️
ProcessWebhookJobreliable processing
Transaction UpdatedGatewayResponseDTO
📣
Events FiredPaymentCaptured

📦 Contracts (Interfaces)

  • PaymentDriverInterface
  • TransactionRepositoryInterface
  • ReconciliationInterface
  • WebhookHandlerInterface

🔧 Services

  • PaymentManager
  • WebhookProcessor
  • ReconciliationEngine
  • RecoveryEngine
  • BackfillEngine

🚌 Queue Jobs

  • ProcessWebhookJob
  • ReconcileTransactionsJob
  • RecoverStaleTransactionsJob

🎲 Design Patterns

  • Strategy — PaymentDriverInterface
  • Repository — TransactionRepository
  • Adapter — AbstractPaymentDriver
  • DTO — InitiatePaymentDTO
  • Event Sourcing — Audit Log

Usage

Code Examples

Production-ready patterns for the most common use cases.

use Mostafax\PaymentEngine\Facades\PaymentEngine;
use Mostafax\PaymentEngine\DTOs\InitiatePaymentDTO;
use Illuminate\Support\Str;

// ① Build DTO — typed, immutable, no raw env() calls
$dto = InitiatePaymentDTO::fromArray([
    'gateway'     => 'knet',
    'track_id'    => (string) Str::ulid(),
    'amount'      => 25.500,
    'currency'    => 'KWD',
    'success_url' => route('payment.return'),
    'error_url'   => route('payment.cancel'),
    'metadata'    => ['order_id' => $order->id],
]);

// ② Initiate — saves Transaction, gets redirect URL
$result = PaymentEngine::initiate($dto);

// ③ Redirect user to gateway
return redirect($result['redirect_url']);
use Mostafax\PaymentEngine\Facades\PaymentEngine;

// Handles decryption, DB update, event firing automatically
$transaction = PaymentEngine::handleCallback(
    gateway: 'knet',
    payload: $request->all(),
);

if ($transaction->isCaptured()) {
    // fulfill order, send receipt...
}

// Live status inquiry from gateway
$response = PaymentEngine::inquire(
    trackId: $trackId,
    gateway: 'knet',
    amount:  25.500,
);
// → GatewayResponseDTO (status, amount, auth, ref...)
// In EventServiceProvider::boot()

Event::listen(PaymentCaptured::class, function(PaymentCaptured $event) {
    Order::find($event->transaction->metadata['order_id'])
        ?->markAsPaid(amount: $event->transaction->amount);
});

Event::listen(PaymentRecovered::class, function(PaymentRecovered $event) {
    // Auto-recovery happened — notify admin
    Notification::route('mail', config('payment-engine.monitoring.mail_to'))
        ->notify(new PaymentRecoveredNotification($event->transaction));
});

Event::listen(PaymentReconciled::class, function(PaymentReconciled $event) {
    if ($event->report->hasMismatches()) {
        // Send Slack / Telegram alert
    }
});
use Mostafax\PaymentEngine\Services\ReconciliationEngine;

// Synchronous reconciliation
$result = app(ReconciliationEngine::class)->run(
    gateway: 'knet',
    from:    '2026-06-01',
    to:      '2026-06-10',
);

// $result->missingInInternal  — on gateway, missing locally
// $result->amountMismatch      — differs by more than ±0.01
// $result->statusMismatch      — captured on gateway, failed locally

// Artisan
// php artisan payment:reconcile knet --from=2026-06-01 --to=2026-06-10
// php artisan payment:reconcile knet --async
use Mostafax\PaymentEngine\Services\RecoveryEngine;
use Mostafax\PaymentEngine\Services\BackfillEngine;

// Scan & recover stale pending transactions (> 30 min old)
$stats = app(RecoveryEngine::class)->recoverStale(
    olderThanMinutes: 30,
    maxPerRun: 1000,
);
// ['total' => 42, 'recovered' => 39, 'still_pending' => 3]

// Backfill missing transactions from gateway
$stats = app(BackfillEngine::class)->syncMissing(
    gateway: 'knet',
    from: '2026-01-01',
    to:   '2026-01-31',
);
// ['missing' => 7, 'created' => 7, 'errors' => 0]

// Artisan
// php artisan payment:recover --minutes=30 --max=1000
// php artisan payment:sync knet --missing --from=2026-01-01 --to=2026-01-31

REST API

API Endpoints

All endpoints require Laravel Sanctum Bearer Token except webhook receivers.

Method Endpoint Description Auth
POST/api/payment/initiateInitiate payment, returns redirect URLSanctum
GET/api/payment/transactionsPaginated list (filter by gateway, status, date)Sanctum
GET/api/payment/transactions/{ulid}Single transaction with full audit trailSanctum
POST/api/payment/inquire/{trackId}Live gateway status checkSanctum
POST/api/payment/reconcileTrigger reconciliation (sync or async)Sanctum
GET/api/payment/reconciliation/reportsPaginated reconciliation reportsSanctum
GET/api/payment/reconciliation/reports/{ulid}Report with mismatch itemsSanctum
POST/api/payment/webhook/{gateway}Receive push webhooksSig Verified
ANY/payment/{gateway}/successKNET / PayTabs success redirectOpen
ANY/payment/{gateway}/errorKNET / PayTabs error redirectOpen

Database

6-Table Schema

All tables use the pe_ prefix. Names are configurable via config('payment-engine.tables').

pe_transactions

id, ulidPK / unique
tenant_idstring, nullable
gatewaystring(50)
track_idunique
amount, currencydecimal(15,3), char(3)
statusstring(30)
gateway_response, metadatajson
initiated_at, captured_at, failed_attimestamp

pe_transaction_events

idPK
transaction_idFK
event_typestring(60)
from_status, to_statusstring, nullable
actorstring(60)
payloadjson, nullable
created_attimestamp (no updated_at)

pe_webhook_payloads

id, ulidPK / unique
gatewaystring(50)
raw_bodylongText (immutable)
idempotency_keyunique
signature_verifiedboolean
processing_statusstring(30)
attempt_count, next_retry_attinyint / timestamp

pe_webhook_attempts

idPK
webhook_payload_idFK
attempt_numbertinyint
successboolean
exception_messagetext, nullable
exception_tracelongText, nullable

pe_reconciliation_reports

id, ulidPK / unique
gateway, tenant_idstring
period_from, period_todate
matched_count, missing_in_internalunsignedInt
amount_mismatch_countunsignedInt
statusstring(20)

pe_reconciliation_items

idPK
report_idFK
issue_typemissing | amount_mismatch | status_mismatch
gateway_amount / internal_amountdecimal(15,3)
gateway_status / internal_statusstring
auto_recovered, recovered_atboolean / timestamp

Events

6 Domain Events

Listen to payment lifecycle events in your EventServiceProvider to decouple business logic from payment processing.

PaymentInitiated
Fired when a new payment record is created and gateway redirect URL is generated.
$transaction$dto
PaymentCaptured
Fired when gateway confirms successful capture. Use to fulfill orders, send receipts.
$transaction$response
PaymentFailed
Fired when payment is cancelled or declined. Use for retry prompts or logging.
$transaction$response
PaymentRefunded
Fired on successful refund. Carries amount and reason for downstream processing.
$transaction$amount$reason
PaymentRecovered
Fired by RecoveryEngine when a stale pending transaction is successfully resolved via gateway inquiry.
$transaction$response
PaymentReconciled
Fired on reconciliation completion. Check $report->hasMismatches() to trigger alerts.
$report$result

Artisan

Artisan Commands

php artisan payment:sync knet

Full sync of all KNET transactions today from gateway to local database.

php artisan payment:sync knet --from=2026-01-01 --to=2026-01-31

Sync all KNET transactions in a date range. Essential for historical backfill.

php artisan payment:sync knet --missing

Only create records missing locally. Safe to re-run — skips existing records.

php artisan payment:reconcile knet --from=2026-06-01 --to=2026-06-10

Run synchronous reconciliation and print results table with mismatch counts.

php artisan payment:reconcile knet --async

Dispatch reconciliation as a background job on the payment-reconcile queue.

php artisan payment:recover --minutes=30 --max=1000

Scan and recover stale pending transactions older than 30 minutes, up to 1000 per run.


Installation

Get Started in 2 Commands

# 1. Install
composer require mostafax/payment-engine

# 2. That's it — one command does everything automatically
php artisan payment:install

payment:install automatically: publishes config, runs migrations, patches CSRF exclusions (Laravel 10 & 11+), appends .env stubs.

Fill Gateway Credentials in .env

The install command appends stubs to .env — just fill in your real credentials.

# Default gateway
PAYMENT_GATEWAY=knet

# KNET (Kuwait)
KNET_TRANSPORT_ID=your_transport_id
KNET_TRANSPORT_PASSWORD=your_password
KNET_RESOURCE_KEY=your_resource_key
KNET_SUCCESS_URL=https://yourapp.com/payment/knet/success
KNET_ERROR_URL=https://yourapp.com/payment/knet/error
KNET_SANDBOX=true

# Fawry (Egypt)
FAWRY_MERCHANT_CODE=your_merchant_code
FAWRY_SECURE_KEY=your_secure_key
FAWRY_RETURN_URL=https://yourapp.com/payment/fawry/success
FAWRY_SANDBOX=true

Start Queue Workers

Run dedicated workers for each queue in production using Supervisor.

php artisan queue:work --queue=payment-webhooks
php artisan queue:work --queue=payment-reconcile
php artisan queue:work --queue=payment-recovery

Listen to Events

Register event listeners in EventServiceProvider::boot() to fulfill orders, send receipts, or trigger alerts.

Event::listen(PaymentCaptured::class, function (PaymentCaptured $e) {
    Order::find($e->transaction->metadata['order_id'])?->markAsPaid();
});

Author

Built by Mostafa Elbayyar

👨‍💻
Mostafa Elbayyar
Senior Software Engineer & Laravel Package Author