Multi-gateway payments, immutable transaction ledger, webhook reliability, auto-reconciliation, and recovery engine — all in one Laravel package.
Driver-based architecture using the Strategy Pattern. Add new gateways by implementing PaymentDriverInterface — zero changes to core code.
Built for high transaction volume, financial traceability, and production environments that can't afford data loss.
pe_transactions with a ULID and full audit trail in pe_transaction_events. Status changes are append-only — nothing is ever deleted.
PaymentRecovered event fired on success.
PaymentCaptured to update your orders, PaymentRecovered for notifications.
tenant_id. Configure a tenant_resolver callable in config to automatically scope all operations per tenant.
declare(strict_types=1).
PaymentDriverInterface, TransactionRepositoryInterface, ReconciliationInterface. Readonly DTOs. Final services. Every dependency is injected and mockable.
Every payment follows a deterministic, traceable path through the system.
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
All endpoints require Laravel Sanctum Bearer Token except webhook receivers.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/payment/initiate | Initiate payment, returns redirect URL | Sanctum |
| GET | /api/payment/transactions | Paginated list (filter by gateway, status, date) | Sanctum |
| GET | /api/payment/transactions/{ulid} | Single transaction with full audit trail | Sanctum |
| POST | /api/payment/inquire/{trackId} | Live gateway status check | Sanctum |
| POST | /api/payment/reconcile | Trigger reconciliation (sync or async) | Sanctum |
| GET | /api/payment/reconciliation/reports | Paginated reconciliation reports | Sanctum |
| GET | /api/payment/reconciliation/reports/{ulid} | Report with mismatch items | Sanctum |
| POST | /api/payment/webhook/{gateway} | Receive push webhooks | Sig Verified |
| ANY | /payment/{gateway}/success | KNET / PayTabs success redirect | Open |
| ANY | /payment/{gateway}/error | KNET / PayTabs error redirect | Open |
All tables use the pe_ prefix. Names are configurable via config('payment-engine.tables').
Listen to payment lifecycle events in your EventServiceProvider to decouple business logic from payment processing.
$report->hasMismatches() to trigger alerts.
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.
# 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.
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
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
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(); });