-
Notifications
You must be signed in to change notification settings - Fork 3
Installation.md
👋 New to PayZephyr? Check out our Getting Started Guide for a complete step-by-step tutorial!
- PHP 8.2 or higher
- Laravel 10.x, 11.x, or 12.x
- Composer
# 1. Install the package
composer require kendenigerian/payzephyr
# 2. Run the install command (publishes config, migrations, and optionally runs migrations)
php artisan payzephyr:install
# 3. Configure your environment variables (see below)That's it! You're ready to start accepting payments.
💡 Alternative: If you prefer manual setup:
php artisan vendor:publish --tag=payments-config php artisan vendor:publish --tag=payments-migrations php artisan migrate
Add your provider credentials to .env:
# Default Provider
PAYMENTS_DEFAULT_PROVIDER=paystack
PAYMENTS_FALLBACK_PROVIDER=stripe
# Paystack (Required: secret_key, public_key)
PAYSTACK_SECRET_KEY=sk_test_xxxxx
PAYSTACK_PUBLIC_KEY=pk_test_xxxxx
PAYSTACK_ENABLED=true
# Stripe (Required: secret_key, public_key, webhook_secret)
STRIPE_SECRET_KEY=sk_test_xxxxx
STRIPE_PUBLIC_KEY=pk_test_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
STRIPE_ENABLED=false
# Optional Security Settings
PAYMENTS_RATE_LIMIT_ENABLED=true
PAYMENTS_RATE_LIMIT_ATTEMPTS=10
PAYMENTS_WEBHOOK_TIMESTAMP_TOLERANCE=300📖 See Configuration Guide for complete details.
use KenDeNigerian\PayZephyr\Facades\Payment;
use KenDeNigerian\PayZephyr\Exceptions\ChargeException;
try {
// Redirect user to the payment page
return Payment::amount(10000)
->email('customer@example.com')
->callback(route('payment.callback'))
->redirect();
} catch (ChargeException $e) {
// Handle payment initialization failure
return back()->with('error', 'Payment initialization failed: ' . $e->getMessage());
}try {
return payment()
->amount(10000)
->email('customer@example.com')
->callback(route('payment.callback'))
->redirect();
} catch (\Exception $e) {
return back()->with('error', $e->getMessage());
}use Illuminate\Support\Str;
use KenDeNigerian\PayZephyr\Exceptions\ProviderException;
try {
return Payment::amount(50000)
->currency('NGN')
->email('customer@example.com')
->reference('ORDER_' . time())
->description('Premium subscription')
->idempotency(Str::uuid()->toString())
->metadata(['order_id' => 12345])
->customer(['name' => 'John Doe', 'phone' => '+2348012345678'])
->channels(['card', 'bank_transfer'])
->with('paystack') // Specify provider (optional)
->redirect();
} catch (ProviderException $e) {
// All providers failed
Log::error('Payment failed across all providers', [
'error' => $e->getMessage(),
'exceptions' => $e->getContext()
]);
return back()->with('error', 'Unable to process payment. Please try again later.');
}use KenDeNigerian\PayZephyr\Exceptions\VerificationException;
public function callback(Request $request)
{
$reference = $request->input('reference');
try {
// verify() searches all providers if no provider specified
$verification = Payment::verify($reference);
if ($verification->isSuccessful()) {
// Payment successful - update your database
Order::where('payment_reference', $reference)
->update(['status' => 'paid']);
return view('payment.success', [
'amount' => $verification->amount,
'reference' => $verification->reference,
]);
}
return view('payment.failed', [
'message' => 'Payment was not successful'
]);
} catch (VerificationException $e) {
logger()->error('Payment verification failed', [
'reference' => $reference,
'error' => $e->getMessage(),
]);
return view('payment.error');
}
}Understanding the payment flow helps you integrate PayZephyr effectively:
return Payment::amount(10000)
->email('customer@example.com')
->callback(route('payment.callback'))
->redirect();What happens:
- You build a payment request with fluent methods
-
redirect()creates a checkout session with the provider - Customer is redirected to provider's secure payment page
- Transaction is automatically logged to database
- Customer completes payment on provider's secure page
- Provider processes the transaction
- Customer sees success/failure confirmation
public function callback(Request $request)
{
$reference = $request->input('reference');
$verification = Payment::verify($reference);
if ($verification->isSuccessful()) {
// Update your order status
Order::where('payment_reference', $reference)
->update(['status' => 'paid']);
}
}
⚠️ CRITICAL: Webhooks are processed asynchronously via Laravel's queue system. You MUST run queue workers for webhooks to work:# Production (using supervisor) php artisan queue:work --queue=default --tries=3 # Development php artisan queue:listenWithout queue workers, webhooks will be queued but never processed!
// app/Listeners/HandlePaystackWebhook.php
public function handle(array $payload): void
{
if ($payload['event'] === 'charge.success') {
$reference = $payload['data']['reference'];
// Update order status (idempotent - safe to run multiple times)
Order::where('payment_reference', $reference)
->update(['status' => 'paid']);
}
}Important: Webhooks can arrive BEFORE or AFTER the customer returns to your callback URL. Always design your callback and webhook handlers to be idempotent (safe to run multiple times).
Configure these in your provider dashboards:
-
Paystack:
https://yourdomain.com/payments/webhook/paystack -
Flutterwave:
https://yourdomain.com/payments/webhook/flutterwave -
Monnify:
https://yourdomain.com/payments/webhook/monnify -
Stripe:
https://yourdomain.com/payments/webhook/stripe -
PayPal:
https://yourdomain.com/payments/webhook/paypal -
Square:
https://yourdomain.com/payments/webhook/square
// app/Providers/EventServiceProvider.php
protected $listen = [
'payments.webhook.paystack' => [
\App\Listeners\HandlePaystackWebhook::class,
],
'payments.webhook' => [
\App\Listeners\HandleAnyWebhook::class,
],
];namespace App\Listeners;
class HandlePaystackWebhook
{
public function handle(array $payload): void
{
$event = $payload['event'] ?? null;
match($event) {
'charge.success' => $this->handleSuccess($payload['data']),
'charge.failed' => $this->handleFailure($payload['data']),
default => logger()->info("Unhandled event: {$event}"),
};
}
private function handleSuccess(array $data): void
{
$reference = $data['reference'];
// Idempotent update (safe to run multiple times)
DB::transaction(function () use ($reference) {
$order = Order::where('payment_reference', $reference)
->lockForUpdate()
->first();
if ($order && $order->status !== 'paid') {
$order->update(['status' => 'paid', 'paid_at' => now()]);
Mail::to($order->customer_email)->send(new OrderConfirmation($order));
}
});
}
}📖 For complete webhook documentation, see docs/webhooks.md
| Provider | Charge | Verify | Webhooks | Idempotency | Channels | Currencies |
|---|---|---|---|---|---|---|
| Paystack | ✅ | ✅ | ✅ | ✅ | 5 | NGN, GHS, ZAR, USD |
| Flutterwave | ✅ | ✅ | ✅ | ✅ | 10+ | NGN, USD, EUR, GBP, KES, UGX, TZS |
| Monnify | ✅ | ✅ | ✅ | ✅ | 4 | NGN |
| Stripe | ✅ | ✅ | ✅ | ✅ | 6+ | 135+ currencies |
| PayPal | ✅ | ✅ | ✅ | ❌ | 1 | USD, EUR, GBP, CAD, AUD |
| Square | ✅ | ✅ | ✅ | ✅ | 4 | USD, CAD, GBP, AUD |
| OPay | ✅ | ✅ | ✅ | ✅ | 5 | NGN |
Notes:
- ✅ = Fully supported
- ❌ = Not supported by provider
- Channels: Number of payment methods (card, bank transfer, USSD, etc.)
- Idempotency: Prevents duplicate charges with unique keys
📖 For provider-specific details, see docs/providers.md
All transactions are automatically logged to the payment_transactions table:
use KenDeNigerian\PayZephyr\Models\PaymentTransaction;
// Query transactions
$transactions = PaymentTransaction::where('email', 'user@example.com')
->successful()
->get();
// Check status
$transaction = PaymentTransaction::where('reference', 'ORDER_123')->first();
if ($transaction->isSuccessful()) {
// Process order fulfillment
}
// Available scopes
PaymentTransaction::successful()->get();
PaymentTransaction::failed()->get();
PaymentTransaction::pending()->get();PayZephyr automatically provides session isolation in multi-tenant applications:
When Laravel authentication is active, payment sessions are automatically isolated per user:
// User 1's payment
Auth::loginUsingId(1);
Payment::amount(10000)->charge(); // Cached with user_1 prefix
// User 2's payment (completely isolated)
Auth::loginUsingId(2);
Payment::amount(20000)->charge(); // Cached with user_2 prefixCurrent Support:
- ✅ User-based isolation (via Laravel auth)
- ✅ Session-based isolation
- ✅ IP-based rate limiting fallback
All providers support sandbox/test modes:
# Paystack Test Mode
PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxx
PAYSTACK_PUBLIC_KEY=pk_test_xxxxxxxxxxxxx
# Stripe Test Mode
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxx
# Monnify Sandbox
MONNIFY_BASE_URL=https://sandbox.monnify.comuse KenDeNigerian\PayZephyr\Facades\Payment;
use Illuminate\Foundation\Testing\RefreshDatabase;
class PaymentTest extends TestCase
{
use RefreshDatabase;
public function test_payment_initialization()
{
$response = Payment::amount(10000)
->email('test@example.com')
->callback('https://example.com/callback')
->charge();
$this->assertNotEmpty($response->reference);
$this->assertNotEmpty($response->authorizationUrl);
$this->assertEquals('pending', $response->status);
}
public function test_payment_verification()
{
// Use real test credentials to verify against sandbox
$response = Payment::amount(10000)
->email('test@example.com')
->callback('https://example.com/callback')
->charge();
// In real tests, you'd complete payment on provider's test page
// For now, we just verify the structure
$this->assertIsString($response->reference);
}
}public function test_webhook_processing()
{
// Mock webhook payload
$payload = [
'event' => 'charge.success',
'data' => [
'reference' => 'TEST_123',
'amount' => 1000000, // 10,000 in kobo
'status' => 'success',
],
];
// Send webhook request
$response = $this->postJson('/payments/webhook/paystack', $payload, [
'x-paystack-signature' => $this->generateSignature($payload),
]);
$response->assertStatus(202); // Queued
// Process queue
Queue::fake();
$this->artisan('queue:work --once');
// Assert transaction updated
$this->assertDatabaseHas('payment_transactions', [
'reference' => 'TEST_123',
'status' => 'success',
]);
}📖 For complete testing guide, see docs/DOCUMENTATION.md#testing
// Try Paystack first, fallback to Stripe if it fails
return Payment::amount(10000)
->email('customer@example.com')
->with(['paystack', 'stripe'])
->redirect();use KenDeNigerian\PayZephyr\PaymentManager;
$manager = app(PaymentManager::class);
$driver = $manager->driver('paystack');
// Check health
if ($driver->healthCheck()) {
// Provider is available
}
// Check currency support
if ($driver->isCurrencySupported('NGN')) {
// Currency supported
}// Get payment details without redirecting
$response = Payment::amount(10000)
->email('customer@example.com')
->callback(route('payment.callback'))
->with('stripe')
->charge(); // Returns ChargeResponseDTO
return response()->json([
'reference' => $response->reference,
'authorization_url' => $response->authorizationUrl,
'status' => $response->status,
]);📖 For advanced patterns, see docs/architecture.md
Symptoms: Webhooks arrive but transactions don't update
Solution:
# Make sure queue workers are running
php artisan queue:work
# Check failed jobs
php artisan queue:failed
# Retry failed jobs
php artisan queue:retry allSymptoms: DriverNotFoundException
Solution:
# Ensure provider is enabled
PAYSTACK_ENABLED=true
# Check credentials are set
PAYSTACK_SECRET_KEY=sk_test_xxxxxSymptoms: getCachedHealthCheck() returns false
Solution:
// Bypass cache to check real status
$driver = app(PaymentManager::class)->driver('paystack');
$isHealthy = $driver->healthCheck(); // Direct check
// Clear health check cache
Cache::forget('payments.health.paystack');Symptoms: "Too many payment attempts" error
Solution:
// Clear rate limit for testing
RateLimiter::clear('payment_charge:user_1');
// Or adjust limits in config
'rate_limit' => [
'max_attempts' => 20, // Increase limit
'decay_seconds' => 120, // Longer window
],PayZephyr provides a built-in health check endpoint to monitor provider availability:
Endpoint: GET /payments/health
Response:
{
"status": "operational",
"providers": {
"paystack": {
"healthy": true,
"currencies": ["NGN", "USD", "GHS", "ZAR"]
},
"stripe": {
"healthy": true,
"currencies": ["USD", "EUR", "GBP", "CAD", "AUD"]
},
"flutterwave": {
"healthy": false,
"currencies": ["NGN", "USD", "EUR", "GBP"]
}
}
}Usage:
- Monitor provider health in your application
- Set up uptime monitoring (e.g., UptimeRobot, Pingdom)
- Check provider availability before processing payments
- Health checks are cached (default: 5 minutes) to avoid excessive API calls
Configuration:
# Adjust cache TTL (in seconds)
PAYMENTS_HEALTH_CHECK_CACHE_TTL=300Symptoms: Webhooks return 403 Unauthorized
Solution:
# Ensure correct webhook secret
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx
# Not the same as API secret key!
# Get from provider dashboard → Webhooks → Signing SecretEnable detailed logging:
# Enable query logging
DB_LOG_QUERIES=true
# Increase log level
LOG_LEVEL=debug
# Check logs
storage/logs/laravel.log- Review complete documentation
- Search GitHub issues
- Open a new issue with:
- Laravel version
- PHP version
- PayZephyr version
- Provider name
- Error message and stack trace
- Steps to reproduce
- Getting Started Guide ⭐ Start here if you're new!
- Complete Documentation - Comprehensive guide
- Installation & Setup - You are here
- Architecture Guide - System design
- API Reference - Complete API docs
- Provider Details - Provider-specific information
- Webhook Guide - Complete webhook documentation
- Contributing Guide for Beginners ⭐ New to open source?
- Contributing Guidelines - Technical contribution guide
Contributions are welcome! Please see:
- CONTRIBUTING_GUIDE.md - Step-by-step guide for beginners
- CONTRIBUTING.md - Technical guidelines
Key areas for contribution:
- Adding new payment providers
- Improving test coverage
- Enhancing documentation
- Reporting bugs
- Suggesting features
Please see CHANGELOG.md for recent changes.
- Fixed all PHPStan static analysis errors
- Improved type safety across the codebase
- Enhanced code quality and maintainability
- Added comprehensive test coverage improvements (855 tests, 1,707 assertions)
- Better IDE support with enhanced PHPDoc annotations
- CRITICAL: SQL injection prevention in table name validation
- CRITICAL: Webhook replay attack prevention with timestamp validation (all drivers)
- CRITICAL: Multi-tenant cache isolation
- HIGH: Automatic log sanitization for sensitive data
- HIGH: Rate limiting for payment initialization
- Enhanced input validation (email, URL, reference format)
- Security configuration section in config
- Comprehensive security test suite (85+ tests)
- Security guide documentation
- Enhanced webhook timestamp validation for all providers
- Added comprehensive Security Guide
- Updated all documentation with security best practices
- Enhanced troubleshooting section
See CHANGELOG.md for complete details.
The MIT License (MIT). Please see LICENSE for more information.
If PayZephyr helped your project:
- ⭐ Star the repository on GitHub
- 🐦 Tweet about it
- 📝 Write a blog post
- 💰 Sponsor the project
- 🤝 Contribute code or documentation
Built with ❤️ for the Laravel community by Ken De Nigerian