A comprehensive Laravel package for integrating PayU payment gateway with complete database transaction tracking, webhook handling, and advanced payment management features.
- π Complete PayU API Integration - All PayU APIs wrapped in a Laravel-friendly interface
- πΎ Database Transaction Tracking - Automatic logging of payments, refunds, and webhooks
- π Enhanced Data Integrity - Database enums for status fields and categorized data
- π― Eloquent Models - Rich models with relationships and helper methods
- π§ͺ Comprehensive Testing - 28+ unit tests covering all functionality
- π Webhook Management - Automatic webhook processing and verification
- π‘οΈ Security First - Hash verification and secure configuration management
- π Analytics Ready - Query scopes and aggregation methods for reporting
composer require sarfarazstark/laravel-payu
# Publish configuration file
php artisan vendor:publish --tag=payu-config
# Publish migration files
php artisan vendor:publish --tag=payu-migrations
# Publish Eloquent models (optional)
php artisan vendor:publish --tag=payu-models
# Run migrations to create database tables
php artisan migrate
π‘ Note: Publishing models is optional. You can work with the PayU transactions, refunds, and webhooks using the package's built-in models (
SarfarazStark\LaravelPayU\Models\*
). However, if you want to customize the models or add additional relationships in your application, publish them to yourapp/Models
directory.
Once you publish the models with php artisan vendor:publish --tag=payu-models
, you'll have three Eloquent models in your app/Models
directory:
- Location:
app/Models/PayUTransaction.php
- Features: Complete transaction tracking with relationships and helper methods
- Constants: Status and payment mode constants for easy reference
- Location:
app/Models/PayURefund.php
- Features: Refund management with status tracking and relationships
- Constants: Refund status and type constants
- Location:
app/Models/PayUWebhook.php
- Features: Webhook processing and verification with event tracking
- Constants: Event type and status constants
Example usage with published models:
use App\Models\PayUTransaction;
use App\Models\PayURefund;
use App\Models\PayUWebhook;
// Using published models works exactly the same as package models
$transaction = PayUTransaction::where('txnid', 'TXN123')->first();
$refunds = PayURefund::successful()->get();
$webhooks = PayUWebhook::verified()->recent()->get();
Add your PayU credentials to .env
:
PAYU_KEY=your_payu_merchant_key
PAYU_SALT=your_payu_salt_key
PAYU_ENV_PROD=false
PAYU_SUCCESS_URL=https://your-site.com/payu/success
PAYU_FAILURE_URL=https://your-site.com/payu/failure
β οΈ Important: ThePAYU_SUCCESS_URL
andPAYU_FAILURE_URL
are required. If not set in your .env file, you must passsurl
andfurl
parameters when calling payment methods, otherwise anInvalidArgumentException
will be thrown.
The package creates three optimized database tables:
- Status Enum:
pending
,success
,failure
,cancelled
,failed
- Payment Mode Enum:
CC
,DC
,NB
,UPI
,EMI
,WALLET
,CASH
- Complete transaction details with customer information
- Indexed for performance on status, email, and payment dates
- Status Enum:
pending
,success
,failed
,cancelled
,processing
- Type Enum:
refund
,cancel
,chargeback
- Linked to transactions with refund tracking
- Event Type Enum: 9 different payment/refund event types
- Status Enum:
received
,processed
,failed
,ignored
- Automatic webhook verification and processing logs
<?php
use SarfarazStark\LaravelPayU\Facades\PayU;
use SarfarazStark\LaravelPayU\Models\PayUTransaction;
class PaymentController extends Controller
{
public function initiatePayment(Request $request)
{
// Generate unique transaction ID
$txnid = 'TXN' . time() . rand(1000, 9999);
// Prepare payment parameters
$params = [
'txnid' => $txnid,
'amount' => $request->amount,
'productinfo' => $request->product_name,
'firstname' => $request->first_name,
'lastname' => $request->last_name,
'email' => $request->email,
'phone' => $request->phone,
'address1' => $request->address,
'city' => $request->city,
'state' => $request->state,
'country' => 'India',
'zipcode' => $request->zipcode,
'udf1' => $request->user_id, // Custom field for user tracking
// Optional: Override default URLs from config
// 'surl' => route('payment.success'),
// 'furl' => route('payment.failure'),
];
// Save transaction to database
PayUTransaction::create([
'txnid' => $txnid,
'amount' => $request->amount,
'productinfo' => $request->product_name,
'firstname' => $request->first_name,
'lastname' => $request->last_name,
'email' => $request->email,
'phone' => $request->phone,
'status' => PayUTransaction::STATUS_PENDING,
'payment_initiated_at' => now(),
]);
// Generate payment form
$paymentForm = PayU::showPaymentForm($params);
return view('payment.form', compact('paymentForm'));
}
}
public function paymentSuccess(Request $request)
{
// Verify hash for security
if (!PayU::verifyHash($request->all())) {
return redirect()->route('payment.failure')
->with('error', 'Payment verification failed');
}
// Update transaction in database
$transaction = PayUTransaction::where('txnid', $request->txnid)->first();
if ($transaction) {
$transaction->update([
'status' => PayUTransaction::STATUS_SUCCESS,
'mihpayid' => $request->mihpayid,
'payment_mode' => $request->mode,
'bankcode' => $request->bankcode,
'bank_ref_num' => $request->bank_ref_num,
'payment_date' => now(),
'response_data' => $request->all(),
]);
return view('payment.success', compact('transaction'));
}
return redirect()->route('payment.failure');
}
public function paymentFailure(Request $request)
{
$transaction = PayUTransaction::where('txnid', $request->txnid)->first();
if ($transaction) {
$transaction->update([
'status' => PayUTransaction::STATUS_FAILED,
'error' => $request->error,
'error_message' => $request->error_Message,
'response_data' => $request->all(),
]);
}
return view('payment.failure', compact('transaction'));
}
use SarfarazStark\LaravelPayU\Models\PayUTransaction;
// Get all successful transactions for a user
$userTransactions = PayUTransaction::successful()
->where('email', 'user@example.com')
->with('refunds', 'webhooks')
->orderBy('created_at', 'desc')
->get();
// Check transaction status with helper methods
$transaction = PayUTransaction::find(1);
if ($transaction->isSuccessful()) {
echo "Payment completed successfully";
} elseif ($transaction->isPending()) {
echo "Payment is still processing";
} elseif ($transaction->isFailed()) {
echo "Payment failed: " . $transaction->error_message;
}
// Get transactions by payment mode
$creditCardTransactions = PayUTransaction::where('payment_mode', PayUTransaction::MODE_CC)->get();
$upiTransactions = PayUTransaction::where('payment_mode', PayUTransaction::MODE_UPI)->get();
// Analytics and reporting
$monthlyRevenue = PayUTransaction::successful()
->whereMonth('created_at', now()->month)
->sum('amount');
$paymentModeStats = PayUTransaction::successful()
->selectRaw('payment_mode, COUNT(*) as count, SUM(amount) as total')
->groupBy('payment_mode')
->get();
use SarfarazStark\LaravelPayU\Models\PayURefund;
// Check if transaction can be refunded
$transaction = PayUTransaction::find(1);
if ($transaction->canBeRefunded()) {
$maxRefundable = $transaction->getRemainingRefundableAmount();
// Create refund record
$refund = PayURefund::create([
'refund_id' => PayURefund::generateRefundId(),
'txnid' => $transaction->txnid,
'amount' => 50.00,
'status' => PayURefund::STATUS_PENDING,
'type' => PayURefund::TYPE_REFUND,
'reason' => 'Customer requested refund',
'refund_requested_at' => now(),
]);
// Process refund via PayU API
$refundResponse = PayU::cancelRefundTransaction([
'txnid' => $transaction->txnid,
'refund_amount' => 50.00,
'reason' => 'Customer request'
]);
// Update refund status based on API response
if ($refundResponse['status'] === 'success') {
$refund->markAsSuccessful($refundResponse['refund_id']);
} else {
$refund->markAsFailed($refundResponse['error']);
}
}
// Get all refunds for a transaction
$refunds = $transaction->refunds()->successful()->get();
$totalRefunded = $transaction->getTotalRefundedAttribute();
use SarfarazStark\LaravelPayU\Models\PayUWebhook;
public function handleWebhook(Request $request)
{
// Create webhook record
$webhook = PayUWebhook::create([
'webhook_id' => PayUWebhook::generateWebhookId(),
'txnid' => $request->txnid,
'event_type' => $request->event_type,
'status' => PayUWebhook::STATUS_RECEIVED,
'payload' => $request->all(),
'headers' => $request->headers->all(),
'received_at' => now(),
]);
try {
// Verify webhook authenticity
if ($this->verifyWebhookSignature($request)) {
$webhook->update(['verified' => true]);
// Process webhook based on event type
switch ($webhook->event_type) {
case PayUWebhook::EVENT_PAYMENT_SUCCESS:
$this->handlePaymentSuccess($webhook);
break;
case PayUWebhook::EVENT_REFUND_SUCCESS:
$this->handleRefundSuccess($webhook);
break;
// Handle other event types...
}
$webhook->markAsProcessed();
} else {
$webhook->markAsIgnored('Invalid signature');
}
} catch (Exception $e) {
$webhook->markAsFailed($e->getMessage());
}
return response()->json(['status' => 'ok']);
}
// Helper methods for webhook event handling
private function handlePaymentSuccess(PayUWebhook $webhook)
{
$transaction = $webhook->transaction;
if ($transaction && $transaction->status === PayUTransaction::STATUS_PENDING) {
$transaction->update([
'status' => PayUTransaction::STATUS_SUCCESS,
'payment_date' => now(),
]);
// Trigger success events, send emails, etc.
event(new PaymentSuccessEvent($transaction));
}
}
// Transaction Status Constants
PayUTransaction::STATUS_PENDING // 'pending'
PayUTransaction::STATUS_SUCCESS // 'success'
PayUTransaction::STATUS_FAILURE // 'failure'
PayUTransaction::STATUS_CANCELLED // 'cancelled'
PayUTransaction::STATUS_FAILED // 'failed'
// Payment Mode Constants
PayUTransaction::MODE_CC // 'CC' (Credit Card)
PayUTransaction::MODE_DC // 'DC' (Debit Card)
PayUTransaction::MODE_NB // 'NB' (Net Banking)
PayUTransaction::MODE_UPI // 'UPI'
PayUTransaction::MODE_EMI // 'EMI'
PayUTransaction::MODE_WALLET // 'WALLET'
PayUTransaction::MODE_CASH // 'CASH'
// Refund Status Constants
PayURefund::STATUS_PENDING // 'pending'
PayURefund::STATUS_SUCCESS // 'success'
PayURefund::STATUS_FAILED // 'failed'
PayURefund::STATUS_CANCELLED // 'cancelled'
PayURefund::STATUS_PROCESSING // 'processing'
// Refund Type Constants
PayURefund::TYPE_REFUND // 'refund'
PayURefund::TYPE_CANCEL // 'cancel'
PayURefund::TYPE_CHARGEBACK // 'chargeback'
// Get all available values
$statuses = PayUTransaction::getStatuses(); // Array of all status options
$paymentModes = PayUTransaction::getPaymentModes(); // Array of all payment modes
$refundTypes = PayURefund::getTypes(); // Array of all refund types
// Verify payment status
$verification = PayU::verifyPayment(['txnid' => 'TXN123456']);
// Get transaction details
$details = PayU::getTransaction(['txnid' => 'TXN123456']);
// Check payment status
$status = PayU::checkPayment(['txnid' => 'TXN123456']);
// Get checkout page details
$checkoutDetails = PayU::getCheckoutDetails(['var1' => 'value']);
// Cancel/Refund transaction
$refund = PayU::cancelRefundTransaction([
'txnid' => 'TXN123456',
'refund_amount' => 100.00,
'reason' => 'Customer request'
]);
// Check refund status
$refundStatus = PayU::checkRefundStatus(['request_id' => 'REF123']);
// Check EMI eligible bins
$emiBins = PayU::checkEmiEligibleBins(['bin' => '512345']);
// Get EMI amount details
$emiAmount = PayU::getEmiAmount([
'amount' => 10000,
'bank' => 'HDFC',
'tenure' => 6
]);
// Get card bin details
$binDetails = PayU::getBinDetails(['cardnum' => '512345']);
// Validate UPI ID
$upiValidation = PayU::validateUpi(['vpa' => 'user@paytm']);
// Check netbanking status
$netbankingStatus = PayU::getNetbankingStatus();
// Get issuing bank status
$bankStatus = PayU::getIssuingBankStatus();
// Create payment invoice
$invoice = PayU::createPaymentInvoice([
'txnid' => 'INV123456',
'amount' => 1000,
'email' => 'customer@example.com',
'phone' => '9999999999',
'productinfo' => 'Service Invoice',
'firstname' => 'John Doe'
]);
// Expire payment invoice
$expiry = PayU::expirePaymentInvoice(['txnid' => 'INV123456']);
// Daily transaction summary
$dailyStats = PayUTransaction::selectRaw('
DATE(created_at) as date,
COUNT(*) as total_transactions,
SUM(CASE WHEN status = "success" THEN 1 ELSE 0 END) as successful,
SUM(CASE WHEN status = "success" THEN amount ELSE 0 END) as revenue,
AVG(CASE WHEN status = "success" THEN amount ELSE NULL END) as avg_amount
')
->groupBy('date')
->orderBy('date', 'desc')
->limit(30)
->get();
// Payment mode performance
$modePerformance = PayUTransaction::selectRaw('
payment_mode,
COUNT(*) as transactions,
SUM(amount) as total_amount,
AVG(amount) as avg_amount,
SUM(CASE WHEN status = "success" THEN 1 ELSE 0 END) / COUNT(*) * 100 as success_rate
')
->whereNotNull('payment_mode')
->groupBy('payment_mode')
->get();
// Failed transaction analysis
$failureReasons = PayUTransaction::failed()
->selectRaw('error, COUNT(*) as count')
->groupBy('error')
->orderBy('count', 'desc')
->get();
// Monthly refund trends
$refundTrends = PayURefund::selectRaw('
YEAR(created_at) as year,
MONTH(created_at) as month,
COUNT(*) as refund_requests,
SUM(CASE WHEN status = "success" THEN amount ELSE 0 END) as refunded_amount,
AVG(TIMESTAMPDIFF(HOUR, refund_requested_at, refund_processed_at)) as avg_processing_hours
')
->groupBy('year', 'month')
->orderBy('year', 'desc')
->orderBy('month', 'desc')
->get();
The package includes comprehensive unit tests covering all functionality:
# Run all tests
./vendor/bin/phpunit
# Run with coverage report
./vendor/bin/phpunit --coverage-html coverage
# Run specific test methods
./vendor/bin/phpunit --filter testVerifyPayment
use SarfarazStark\LaravelPayU\Models\PayUTransaction;
use Tests\TestCase;
class PaymentTest extends TestCase
{
public function testTransactionCreation()
{
$transaction = PayUTransaction::create([
'txnid' => 'TEST123',
'amount' => 100.00,
'status' => PayUTransaction::STATUS_PENDING,
// ... other fields
]);
$this->assertDatabaseHas('payu_transactions', [
'txnid' => 'TEST123',
'status' => PayUTransaction::STATUS_PENDING
]);
}
}
// Always verify hash in success/failure callbacks
if (!PayU::verifyHash($request->all())) {
// Handle invalid hash - possible tampering
return response()->json(['error' => 'Invalid hash'], 400);
}
// Use different credentials for staging/production
// .env.staging
PAYU_ENV_PROD=false
PAYU_KEY=test_key
PAYU_SALT=test_salt
// .env.production
PAYU_ENV_PROD=true
PAYU_KEY=live_key
PAYU_SALT=live_salt
// Use fillable arrays to prevent mass assignment
// Models already include proper $fillable arrays
// Use enum constraints for data integrity
// Database migrations include enum constraints for status fields
// Add database indexes for better query performance
// Indexes are already included in migrations for:
// - Transaction status and email lookups
// - Webhook event type filtering
// - Payment date range queries
// Use eager loading to prevent N+1 queries
$transactions = PayUTransaction::with(['refunds', 'webhooks'])
->successful()
->paginate(50);
// Log webhook processing for debugging
Log::info('PayU webhook received', [
'webhook_id' => $webhook->webhook_id,
'event_type' => $webhook->event_type,
'txnid' => $webhook->txnid
]);
// Monitor failed transactions
$recentFailures = PayUTransaction::failed()
->where('created_at', '>=', now()->subHours(24))
->count();
if ($recentFailures > 10) {
// Alert administrators
Mail::to('admin@yoursite.com')->send(new HighFailureRateAlert($recentFailures));
}
We welcome contributions! Please see our contributing guidelines for details.
# Clone the repository
git clone https://github.com/sarfarazstark/laravel-payu.git
# Install dependencies
composer install
# Run tests
./vendor/bin/phpunit
# Check code style
./vendor/bin/php-cs-fixer fix --dry-run
For detailed API documentation and parameter requirements, see the doc/
folder:
- Payment Verification
- Transaction Details
- Refund Operations
- EMI Operations
- UPI Validation
- Invoice Management
- And more...
This package is open-sourced software licensed under the MIT License.
- π§ Email: support@sarfarazstark.com
- π Issues: GitHub Issues
- π Documentation: GitHub Wiki
Made with β€οΈ by Sarfaraz Stark