itul / slick-payment-gateway
A Laravel package for integrating multiple payment gateways with PCI compliant tokenization
Requires
- php: ^8.1
- guzzlehttp/guzzle: >=7.0
- laravel/framework: >=10.0
- paypal/paypal-checkout-sdk: >=1.0
- stripe/stripe-php: >=10.0
Requires (Dev)
- laravel/pint: ^1.15
- nunomaduro/larastan: ^2.9
- orchestra/testbench: ^8.0.0|^9.0
- phpunit/phpunit: ^10.5|^11.0
README
A PCI DSS compliant Laravel package for seamless payment processing with multiple gateway support, built for modern SaaS applications with multi-tenant architecture.
โจ Key Features
- ๐ PCI DSS Compliant - Client-side tokenization only, zero raw card data processing
- ๐ข Multi-Tenant SaaS Ready - Built-in tenant isolation and configuration management
- ๐ณ Multiple Payment Gateways - Stripe, PayPal, Authorize.Net with unified interface
- ๐ Enterprise Security - Laravel encryption, audit logging, role-based access
- ๐ฏ Developer Friendly - Clean APIs, comprehensive docs, extensive testing
- โก Production Ready - Rate limiting, webhook handling, async processing
๐ Quick Start
1. Installation
composer require itul/slick-payment-gateway
2. Publish & Migrate
# Publish configuration
php artisan vendor:publish --tag=slick-payment-gateway-config
# Run migrations
php artisan migrate
3. Configure Environment
# Choose your default gateway
SLICK_PAYMENT_GATEWAY_DRIVER=stripe
# Stripe Configuration
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Optional: Enable multi-tenant mode
SLICK_SASS_MODE_ENABLED=true
4. Your First Payment โก
Stripe Example:
use Itul\SlickPaymentGateway\Facades\SlickPaymentGateway;
use Itul\SlickPaymentGateway\DTOs\PaymentRequest;
// Process a payment (using pre-tokenized payment method)
$payment = SlickPaymentGateway::processPayment(
PaymentRequest::make([
'payment_method_id' => '1', // Your stored payment method ID
'amount' => 29.99,
'currency' => 'USD',
'description' => 'Pro Plan Subscription'
])
);
if ($payment->success) {
echo "Transaction ID: " . $payment->transactionId;
} else {
echo "Error: " . $payment->message;
}
Authorize.Net Example:
// Configure for Authorize.Net
config(['slick-payment-gateway.default_driver' => 'authorize_net']);
$payment = SlickPaymentGateway::processPayment(
PaymentRequest::make([
'payment_method_id' => '1',
'amount' => 99.50,
'currency' => 'USD',
'description' => 'Enterprise Plan - Annual',
'metadata' => [
'invoice_number' => 'INV-2024-001',
'customer_po' => 'PO-12345'
]
])
);
if ($payment->success) {
echo "Auth.Net Transaction: " . $payment->transactionId;
echo "Auth Code: " . $payment->gatewayResponse['authCode'];
} else {
echo "Decline Reason: " . $payment->message;
}
๐ณ Payment Method Management
Creating Payment Methods (Client-Side Tokenization)
Frontend (Authorize.Net Accept.js):
// Create payment nonce with Accept.js (PCI compliant)
const authData = {
clientKey: 'your_client_key',
apiLoginID: 'your_api_login_id'
};
const cardData = {
cardNumber: cardElement.value,
month: expMonth.value,
year: expYear.value,
cardCode: cvv.value
};
Accept.dispatchData(authData, cardData, function(response) {
if (response.messages.resultCode === 'Error') {
// Handle client-side error
console.error('Accept.js Error:', response.messages.message[0].text);
return;
}
// Send payment nonce to backend (PCI compliant)
fetch('/api/payment-methods', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
payment_method_token: response.opaqueData.dataValue,
payment_descriptor: response.opaqueData.dataDescriptor,
owner_type: 'App\\Models\\User',
owner_id: user.id
})
});
});
Frontend (Stripe Elements):
// Create payment method on client-side (PCI compliant)
const {paymentMethod} = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name: 'John Doe',
email: 'john@example.com'
}
});
// Send token to backend
fetch('/api/payment-methods', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
payment_method_token: paymentMethod.id,
owner_type: 'App\\Models\\User',
owner_id: user.id
})
});
Backend Processing:
use Itul\SlickPaymentGateway\Facades\SlickPaymentGateway;
public function storePaymentMethod(Request $request)
{
// Handle Authorize.Net payment nonce
if ($request->has('payment_descriptor')) {
$request->validate([
'payment_method_token' => 'required|string',
'payment_descriptor' => 'required|string',
'owner_type' => 'required|string',
'owner_id' => 'required|integer'
]);
$paymentMethodId = SlickPaymentGateway::attachPaymentMethod(
$request->payment_method_token,
[
'owner_type' => $request->owner_type,
'owner_id' => $request->owner_id,
'payment_descriptor' => $request->payment_descriptor
]
);
}
// Handle other gateway tokens (Stripe, PayPal)
else {
$request->validate([
'payment_method_token' => 'required|string',
'owner_type' => 'required|string',
'owner_id' => 'required|integer'
]);
$paymentMethodId = SlickPaymentGateway::attachPaymentMethod(
$request->payment_method_token,
[
'owner_type' => $request->owner_type,
'owner_id' => $request->owner_id
]
);
}
return response()->json([
'payment_method_id' => $paymentMethodId,
'message' => 'Payment method saved successfully'
]);
}
Using Stored Payment Methods
use Itul\SlickPaymentGateway\Models\SlickPaymentMethod;
// Get user's payment methods
$paymentMethods = SlickPaymentMethod::where('owner_type', 'App\\Models\\User')
->where('owner_id', auth()->id())
->get();
// Display in your UI
foreach ($paymentMethods as $method) {
echo $method->getDisplayName(); // "Visa ending in 4242"
echo $method->isExpired() ? 'Expired' : 'Active';
}
// Make a payment method default
$paymentMethod = SlickPaymentMethod::find(1);
$paymentMethod->makeDefault();
๐ฐ Payment Processing
One-Time Payments
Authorize.Net Example:
use Itul\SlickPaymentGateway\DTOs\PaymentRequest;
use Itul\SlickPaymentGateway\Facades\SlickPaymentGateway;
// Configure for Authorize.Net
config(['slick-payment-gateway.default_driver' => 'authorize_net']);
// Process enterprise payment with detailed metadata
$result = SlickPaymentGateway::processPayment(
PaymentRequest::make([
'payment_method_id' => '1',
'amount' => 2499.99,
'currency' => 'USD',
'description' => 'Enterprise Software License',
'metadata' => [
'invoice_number' => 'INV-2024-0123',
'customer_po' => 'PO-ACME-456',
'department' => 'IT Services',
'billing_period' => 'Q1 2024',
'tax_exempt' => true
]
])
);
// Handle Authorize.Net specific response data
if ($result->success) {
$transactionId = $result->transactionId; // Auth.Net transaction ID
$authCode = $result->gatewayResponse['authCode']; // Authorization code
$avsResponse = $result->gatewayResponse['avsResultCode']; // Address verification
$cvvResponse = $result->gatewayResponse['cvvResultCode']; // CVV verification
// Log enterprise transaction details
Log::info('Enterprise payment processed', [
'transaction_id' => $transactionId,
'auth_code' => $authCode,
'avs_result' => $avsResponse,
'cvv_result' => $cvvResponse,
'invoice_number' => 'INV-2024-0123'
]);
return redirect()->route('payment.success', $transactionId);
} else {
// Handle Authorize.Net specific error codes
$errorCode = $result->gatewayResponse['errorCode'] ?? null;
$errorText = $result->gatewayResponse['errorText'] ?? $result->message;
Log::warning('Authorize.Net payment failed', [
'error_code' => $errorCode,
'error_text' => $errorText,
'invoice_number' => 'INV-2024-0123'
]);
return back()->withErrors(['payment' => "Payment failed: {$errorText}"]);
}
Stripe Example:
// Process payment with stored payment method
$result = SlickPaymentGateway::processPayment(
PaymentRequest::make([
'payment_method_id' => '1',
'amount' => 99.99,
'currency' => 'USD',
'description' => 'Annual Pro Subscription',
'metadata' => [
'customer_id' => auth()->id(),
'plan' => 'pro-annual'
]
])
);
// Handle response
if ($result->success) {
// Payment completed
$transactionId = $result->transactionId;
$status = $result->status; // 'completed', 'pending', etc.
// Redirect user to success page
return redirect()->route('payment.success', $transactionId);
} else {
// Payment failed
return back()->withErrors(['payment' => $result->message]);
}
Refund Processing
Authorize.Net Refund Example:
use Itul\SlickPaymentGateway\DTOs\RefundRequest;
// Authorize.Net requires original transaction details for refunds
$refund = SlickPaymentGateway::refundPayment(
RefundRequest::make([
'transaction_id' => 'authnet_txn_123456',
'amount' => 1500.00, // Partial refund of $2499.99 original
'reason' => 'Partial service cancellation',
'metadata' => [
'original_invoice' => 'INV-2024-0123',
'refund_invoice' => 'REF-2024-0045',
'last_four' => '1234' // Required by Authorize.Net for refunds
]
])
);
if ($refund->success) {
$refundId = $refund->transactionId;
$refundAmount = $refund->gatewayResponse['settleAmount'];
Log::info('Authorize.Net refund processed', [
'refund_id' => $refundId,
'original_transaction' => 'authnet_txn_123456',
'refund_amount' => $refundAmount
]);
} else {
// Handle refund failure - common with Auth.Net batch processing
Log::error('Authorize.Net refund failed', [
'error' => $refund->message,
'transaction_id' => 'authnet_txn_123456'
]);
}
Standard Refund Examples:
// Full refund
$refund = SlickPaymentGateway::refundPayment(
RefundRequest::make([
'transaction_id' => 'txn_123456',
'reason' => 'Customer requested cancellation'
])
);
// Partial refund
$partialRefund = SlickPaymentGateway::refundPayment(
RefundRequest::make([
'transaction_id' => 'txn_123456',
'amount' => 25.00, // Refund $25 of original $100
'reason' => 'Partial service credit'
])
);
Subscription Management
Authorize.Net Recurring Billing (ARB):
use Itul\SlickPaymentGateway\DTOs\SubscriptionRequest;
// Create Authorize.Net ARB subscription
$subscription = SlickPaymentGateway::createSubscription(
SubscriptionRequest::make([
'payment_method_id' => '1',
'amount' => 149.99,
'currency' => 'USD',
'interval' => 'monthly',
'plan_name' => 'Enterprise Plan',
'metadata' => [
'trial_days' => 14,
'setup_fee' => 50.00,
'billing_cycles' => 12, // 1 year contract
'customer_type' => 'business',
'tax_rate' => 8.25
]
])
);
if ($subscription->success) {
$arbSubscriptionId = $subscription->transactionId;
Log::info('Authorize.Net ARB subscription created', [
'arb_subscription_id' => $arbSubscriptionId,
'customer_id' => auth()->id(),
'plan' => 'Enterprise Plan'
]);
}
// Cancel ARB subscription
$cancelled = SlickPaymentGateway::cancelSubscription('arb_sub_123456');
Standard Subscription Example:
// Create subscription
$subscription = SlickPaymentGateway::createSubscription(
SubscriptionRequest::make([
'payment_method_id' => '1',
'amount' => 29.99,
'currency' => 'USD',
'interval' => 'monthly',
'plan_name' => 'Pro Plan'
])
);
// Cancel subscription
$cancelled = SlickPaymentGateway::cancelSubscription('sub_123456');
๐ข Multi-Tenant SaaS Usage
Enable SaaS Mode
// config/slick-payment-gateway.php
'sass_mode' => [
'enabled' => true,
'tenant_model' => App\Models\Company::class,
'tenant_key' => 'company_id', // Key in User model
],
Tenant-Specific Gateway Configuration
use Itul\SlickPaymentGateway\Models\SlickTenantConnection;
// Set up gateway for a tenant
$tenant = Company::find(1);
$connection = SlickTenantConnection::create([
'tenant_type' => Company::class,
'tenant_id' => $tenant->id,
'name' => 'Acme Corp Payment Gateway',
'is_active' => true
]);
// Configure Stripe for this tenant
$connection->setGatewayCredentials('stripe', [
'secret_key' => 'sk_test_tenant_specific_key',
'publishable_key' => 'pk_test_tenant_specific_key',
'webhook_secret' => 'whsec_tenant_specific_secret'
]);
// Configure PayPal for this tenant
$connection->setGatewayCredentials('paypal', [
'client_id' => 'tenant_paypal_client_id',
'client_secret' => 'tenant_paypal_secret',
'mode' => 'sandbox'
]);
// Configure Authorize.Net for this tenant
$connection->setGatewayCredentials('authorize_net', [
'api_login_id' => 'tenant_authnet_login_id',
'transaction_key' => 'tenant_authnet_transaction_key',
'sandbox' => true
]);
Tenant Context in Requests
// In your middleware or service provider
app(SlickTenantConnection::class)->setCurrentTenant($company);
// Or use the SassAware trait in your models
class User extends Model
{
use \Itul\SlickPaymentGateway\Traits\HasSlickPaymentMethods;
public function company()
{
return $this->belongsTo(Company::class);
}
}
// Payments are automatically scoped to the tenant
$userPaymentMethods = $user->slickPaymentMethods; // Only this tenant's methods
๐ Gateway-Specific Features
Authorize.Net Integration
// Authorize.Net enterprise features and metadata
$payment = SlickPaymentGateway::processPayment(
PaymentRequest::make([
'payment_method_id' => '1',
'amount' => 5000.00,
'currency' => 'USD',
'description' => 'Enterprise Software License - Annual',
'metadata' => [
'invoice_number' => 'INV-2024-ENT-001',
'customer_po' => 'PO-ENTERPRISE-789',
'department' => 'IT Infrastructure',
'cost_center' => 'CC-9876',
'billing_contact' => 'John Smith, CFO',
'license_type' => 'enterprise_unlimited',
'contract_term' => '12_months',
'tax_exempt_id' => 'TAX-EXEMPT-12345',
'approval_code' => 'APPROVED-BY-FINANCE',
'recurring_billing' => false,
'duplicate_window' => 300, // 5 minute duplicate transaction window
'customer_ip' => request()->ip(),
'merchant_defined_fields' => [
'field_1' => 'Customer Tier: Enterprise',
'field_2' => 'Sales Rep: Jane Doe',
'field_3' => 'Referral Source: Direct Sales',
'field_4' => 'Contract ID: ENT-2024-001'
]
]
])
);
// Access Authorize.Net specific response data
if ($payment->success) {
$response = $payment->gatewayResponse;
echo "Transaction ID: " . $response['transactionResponse']['transId'];
echo "Auth Code: " . $response['transactionResponse']['authCode'];
echo "Response Code: " . $response['transactionResponse']['responseCode'];
echo "AVS Result: " . $response['transactionResponse']['avsResultCode'];
echo "CVV Result: " . $response['transactionResponse']['cvvResultCode'];
echo "CAVV Result: " . $response['transactionResponse']['cavvResultCode'];
echo "Account Number: " . $response['transactionResponse']['accountNumber']; // Masked
echo "Account Type: " . $response['transactionResponse']['accountType'];
echo "Test Request: " . $response['transactionResponse']['testRequest'];
// Enterprise reporting fields
if (isset($response['transactionResponse']['userFields'])) {
foreach ($response['transactionResponse']['userFields'] as $field) {
echo "Custom Field {$field['name']}: {$field['value']}";
}
}
}
Stripe Integration
// Stripe-specific metadata
$payment = SlickPaymentGateway::processPayment(
PaymentRequest::make([
'payment_method_id' => '1',
'amount' => 50.00,
'metadata' => [
'stripe_account' => 'acct_connected_account_id', // For Stripe Connect
'application_fee' => 2.50, // Platform fee
]
])
);
PayPal Integration
// PayPal-specific configuration
$payment = SlickPaymentGateway::processPayment(
PaymentRequest::make([
'payment_method_id' => '1',
'amount' => 100.00,
'return_url' => route('payment.success'),
'cancel_url' => route('payment.cancel')
])
);
// Handle PayPal redirect
if ($payment->redirectUrl) {
return redirect($payment->redirectUrl);
}
๐ก Webhook Handling
Setup Webhook Endpoints
// routes/web.php - Webhook routes are auto-registered
// Configure webhook URLs in your payment gateway dashboards:
// Stripe: https://yourapp.com/slick/webhooks/stripe
// PayPal: https://yourapp.com/slick/webhooks/paypal
// Authorize.Net: https://yourapp.com/slick/webhooks/authorize-net
Custom Webhook Handlers
use Itul\SlickPaymentGateway\Events\PaymentCompleted;
use Itul\SlickPaymentGateway\Events\PaymentFailed;
use Itul\SlickPaymentGateway\Events\RefundProcessed;
// Listen to payment events
class PaymentEventListener
{
public function handlePaymentCompleted(PaymentCompleted $event)
{
$payment = $event->payment;
// Update your application state
Order::where('transaction_id', $payment->gateway_transaction_id)
->update(['status' => 'paid']);
// Send confirmation email
Mail::to($payment->owner)->send(new PaymentConfirmationMail($payment));
}
public function handlePaymentFailed(PaymentFailed $event)
{
// Handle failed payment
Log::warning('Payment failed', ['payment' => $event->payment]);
// Notify customer
// Update subscription status, etc.
}
}
Manual Webhook Processing
use Itul\SlickPaymentGateway\Support\WebhookDispatcher;
public function handleWebhook(Request $request)
{
$dispatcher = app(WebhookDispatcher::class);
// Handle Authorize.Net Silent Post
if ($request->has('x_trans_id')) {
$result = $dispatcher->dispatch(
gateway: 'authorize_net',
payload: $request->all(),
signature: $request->header('X-ANET-Signature') // Authorize.Net webhook signature
);
}
// Handle Stripe webhook
elseif ($request->header('Stripe-Signature')) {
$result = $dispatcher->dispatch(
gateway: 'stripe',
payload: $request->all(),
signature: $request->header('Stripe-Signature')
);
}
// Handle PayPal webhook
else {
$result = $dispatcher->dispatch(
gateway: 'paypal',
payload: $request->all(),
signature: $request->header('PAYPAL-TRANSMISSION-SIG')
);
}
if ($result) {
return response()->json(['status' => 'success']);
}
return response()->json(['error' => 'Invalid webhook'], 400);
}
๐๏ธ Database Models & Relationships
Core Models
use Itul\SlickPaymentGateway\Models\{
SlickPayment,
SlickPaymentMethod,
SlickBillingContact,
SlickOrder,
SlickInvoice,
SlickSubscription
};
// Payment model with relationships
$payment = SlickPayment::with([
'paymentMethod',
'billingContact',
'order',
'owner' // Polymorphic - User, Company, etc.
])->find(1);
echo $payment->amount; // 99.99
echo $payment->status; // 'completed'
echo $payment->gateway; // 'stripe'
echo $payment->owner->name; // Related model name
Model Relationships
// In your User model
class User extends Model
{
use \Itul\SlickPaymentGateway\Traits\HasSlickPaymentMethods;
// Automatically available relationships:
public function slickPaymentMethods() // HasMany
public function slickPayments() // HasMany
public function slickOrders() // HasMany
public function slickInvoices() // HasMany
public function slickSubscriptions() // HasMany
}
// Usage
$user = User::find(1);
$defaultPaymentMethod = $user->slickPaymentMethods()->where('is_default', true)->first();
$recentPayments = $user->slickPayments()->where('created_at', '>', now()->subDays(30))->get();
Billing Contacts
use Itul\SlickPaymentGateway\Models\SlickBillingContact;
// Create billing contact
$billingContact = SlickBillingContact::create([
'owner_type' => 'App\\Models\\User',
'owner_id' => auth()->id(),
'type' => SlickBillingContact::TYPE_INDIVIDUAL,
'name' => 'John Doe',
'email' => 'john@example.com',
'address_line_1' => '123 Main St',
'city' => 'Sacramento',
'state' => 'CA',
'postal_code' => '95814',
'country' => 'US'
]);
echo $billingContact->getDisplayName(); // "John Doe"
echo $billingContact->getFormattedAddress(); // "123 Main St, Sacramento, CA, 95814, US"
๐จ Admin Interface
Gateway Management UI
The package includes a built-in admin interface for gateway configuration:
// Access via routes (protected by auth middleware)
Route::get('/slick/gateways'); // Gateway management dashboard
Features:
- Connect/disconnect payment gateways
- Test gateway connections
- View gateway status
- Configure webhook endpoints
- Monitor payment activity
Customizing Admin Views
# Publish views for customization
php artisan vendor:publish --tag=slick-payment-gateway-views
Edit published views in resources/views/vendor/slick-payment-gateway/
๐ Security & PCI Compliance
PCI DSS Compliance Features
โ
Requirement 3.1-3.4: No cardholder data storage
โ
Requirement 4.1: Encrypted data transmission
โ
Requirement 7.1: Role-based access controls
โ
Requirement 10.1: Comprehensive audit logging
Security Best Practices
// All sensitive data is encrypted at rest
SlickPaymentMethod::create([
'gateway_token' => 'pm_1234...', // Automatically encrypted
'gateway_customer_id' => 'cus_5678...', // Automatically encrypted
'metadata' => ['key' => 'value'] // Automatically encrypted
]);
// Audit logging is automatic
Log::channel('pci_audit')->info('Payment processed', [
'payment_id' => $payment->id,
'user_id' => auth()->id(),
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent()
]);
Access Control
// In your User model or policy
public function canAccessPaymentData()
{
return $this->hasRole(['admin', 'finance', 'payment_processor']);
}
// Middleware automatically restricts access
Route::middleware(['slick.auth'])->group(function () {
Route::get('/payments', [PaymentController::class, 'index']);
});
โ๏ธ Advanced Configuration
Complete Configuration File
// config/slick-payment-gateway.php
return [
'default_driver' => env('SLICK_PAYMENT_GATEWAY_DRIVER', 'stripe'),
'drivers' => [
'stripe' => [
'secret_key' => env('STRIPE_SECRET_KEY'),
'publishable_key' => env('STRIPE_PUBLISHABLE_KEY'),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
'currency' => env('STRIPE_CURRENCY', 'usd'),
],
'paypal' => [
'client_id' => env('PAYPAL_CLIENT_ID'),
'client_secret' => env('PAYPAL_CLIENT_SECRET'),
'mode' => env('PAYPAL_MODE', 'sandbox'),
'currency' => env('PAYPAL_CURRENCY', 'USD'),
],
'authorize_net' => [
'api_login_id' => env('AUTHORIZE_NET_API_LOGIN_ID'),
'transaction_key' => env('AUTHORIZE_NET_TRANSACTION_KEY'),
'sandbox' => env('AUTHORIZE_NET_SANDBOX', true),
'currency' => env('AUTHORIZE_NET_CURRENCY', 'USD'),
],
],
'sass_mode' => [
'enabled' => env('SLICK_SASS_MODE_ENABLED', false),
'tenant_model' => env('SLICK_TENANT_MODEL', 'App\\Models\\Company'),
'tenant_key' => env('SLICK_TENANT_KEY', 'company_id'),
],
'security' => [
'require_https' => env('SLICK_REQUIRE_HTTPS', true),
'audit_logging' => env('SLICK_AUDIT_LOGGING', true),
'rate_limiting' => [
'payments' => env('SLICK_RATE_LIMIT_PAYMENTS', 60), // per minute
'webhooks' => env('SLICK_RATE_LIMIT_WEBHOOKS', 1000), // per minute
],
],
'features' => [
'subscriptions' => env('SLICK_ENABLE_SUBSCRIPTIONS', true),
'refunds' => env('SLICK_ENABLE_REFUNDS', true),
'partial_refunds' => env('SLICK_ENABLE_PARTIAL_REFUNDS', true),
'saved_payment_methods' => env('SLICK_ENABLE_SAVED_METHODS', true),
],
'ui' => [
'admin_enabled' => env('SLICK_ADMIN_UI_ENABLED', true),
'theme' => env('SLICK_UI_THEME', 'default'),
],
'metrics' => [
'driver' => env('SLICK_METRICS_DRIVER', 'log'), // 'log', 'noop'
'channels' => [
'default' => env('LOG_CHANNEL', 'stack'),
'pci_audit' => env('SLICK_PCI_AUDIT_CHANNEL', 'single'),
],
],
];
Environment Variables
# Core Configuration
SLICK_PAYMENT_GATEWAY_DRIVER=stripe
SLICK_SASS_MODE_ENABLED=false
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_CURRENCY=usd
# PayPal
PAYPAL_CLIENT_ID=your_paypal_client_id
PAYPAL_CLIENT_SECRET=your_paypal_client_secret
PAYPAL_MODE=sandbox
PAYPAL_CURRENCY=USD
# Authorize.Net
AUTHORIZE_NET_API_LOGIN_ID=your_api_login_id
AUTHORIZE_NET_TRANSACTION_KEY=your_transaction_key
AUTHORIZE_NET_SANDBOX=true
AUTHORIZE_NET_CURRENCY=USD
# Multi-Tenant Configuration
SLICK_TENANT_MODEL=App\\Models\\Company
SLICK_TENANT_KEY=company_id
# Security
SLICK_REQUIRE_HTTPS=true
SLICK_AUDIT_LOGGING=true
SLICK_RATE_LIMIT_PAYMENTS=60
SLICK_RATE_LIMIT_WEBHOOKS=1000
# Features
SLICK_ENABLE_SUBSCRIPTIONS=true
SLICK_ENABLE_REFUNDS=true
SLICK_ENABLE_PARTIAL_REFUNDS=true
SLICK_ENABLE_SAVED_METHODS=true
# UI
SLICK_ADMIN_UI_ENABLED=true
SLICK_UI_THEME=default
# Metrics & Logging
SLICK_METRICS_DRIVER=log
SLICK_PCI_AUDIT_CHANNEL=single
๐งช Testing
Running Tests
# Run all tests
php artisan test
# Run specific test suite
php artisan test --testsuite=Feature
php artisan test --testsuite=Unit
# Run with coverage
php artisan test --coverage
Test Your Integration
use Itul\SlickPaymentGateway\Tests\TestCase;
use Itul\SlickPaymentGateway\Facades\SlickPaymentGateway;
class PaymentIntegrationTest extends TestCase
{
public function test_can_process_payment()
{
// Create test payment method
$paymentMethod = $this->createTestPaymentMethod();
// Process payment
$result = SlickPaymentGateway::processPayment(
PaymentRequest::make([
'payment_method_id' => $paymentMethod->id,
'amount' => 25.00,
'currency' => 'USD'
])
);
$this->assertTrue($result->success);
$this->assertNotNull($result->transactionId);
}
}
Mock Responses for Testing
use Itul\SlickPaymentGateway\Testing\MockGatewayResponses;
// In your tests
MockGatewayResponses::fake([
'stripe' => [
'payment_intent.succeeded' => ['id' => 'pi_test_success'],
'payment_method.attached' => ['id' => 'pm_test_method']
]
]);
๐จ Error Handling
Exception Types
use Itul\SlickPaymentGateway\Exceptions\{
PaymentGatewayException,
InvalidPaymentMethodException,
InsufficientFundsException,
GatewayConfigurationException
};
try {
$payment = SlickPaymentGateway::processPayment($request);
} catch (InvalidPaymentMethodException $e) {
// Payment method is expired or invalid
return response()->json(['error' => 'Please update your payment method'], 400);
} catch (InsufficientFundsException $e) {
// Card declined due to insufficient funds
return response()->json(['error' => 'Payment declined - insufficient funds'], 400);
} catch (PaymentGatewayException $e) {
// Generic gateway error
Log::error('Payment gateway error', ['error' => $e->getMessage()]);
return response()->json(['error' => 'Payment processing error'], 500);
}
Graceful Error Handling
// Always check payment response
$result = SlickPaymentGateway::processPayment($request);
if (!$result->success) {
// Log the failure
Log::warning('Payment failed', [
'message' => $result->message,
'gateway_response' => $result->gatewayResponse
]);
// Handle different failure types
if (str_contains($result->message, 'card_declined')) {
return back()->withErrors(['payment' => 'Your card was declined. Please try a different payment method.']);
}
if (str_contains($result->message, 'expired')) {
return back()->withErrors(['payment' => 'Your payment method has expired. Please update your payment information.']);
}
// Generic error
return back()->withErrors(['payment' => 'We encountered an issue processing your payment. Please try again.']);
}
๐ฏ Common Use Cases
E-commerce Integration
class CheckoutController extends Controller
{
public function processCheckout(Request $request)
{
$cart = Cart::forUser(auth()->user());
// Create order
$order = SlickOrder::create([
'customer_type' => 'App\\Models\\User',
'customer_id' => auth()->id(),
'total_amount' => $cart->total(),
'currency' => 'USD',
'status' => SlickOrder::STATUS_PENDING
]);
// Add items to order
foreach ($cart->items as $item) {
$order->items()->create([
'name' => $item->product->name,
'quantity' => $item->quantity,
'unit_price' => $item->unit_price,
'total_price' => $item->total_price
]);
}
// Process payment
$payment = SlickPaymentGateway::processPayment(
PaymentRequest::make([
'payment_method_id' => $request->payment_method_id,
'amount' => $order->total_amount,
'currency' => $order->currency,
'description' => "Order #{$order->id}",
'metadata' => ['order_id' => $order->id]
])
);
if ($payment->success) {
$order->update(['status' => SlickOrder::STATUS_CONFIRMED]);
$cart->clear();
return redirect()->route('order.success', $order);
}
return back()->withErrors(['payment' => $payment->message]);
}
}
Subscription Service
class SubscriptionController extends Controller
{
public function subscribe(Request $request)
{
$plan = Plan::find($request->plan_id);
// Create subscription
$subscription = SlickPaymentGateway::createSubscription(
SubscriptionRequest::make([
'payment_method_id' => $request->payment_method_id,
'amount' => $plan->price,
'currency' => 'USD',
'interval' => $plan->billing_interval, // 'monthly', 'yearly'
'plan_name' => $plan->name,
'metadata' => [
'user_id' => auth()->id(),
'plan_id' => $plan->id
]
])
);
if ($subscription->success) {
// Update user subscription status
auth()->user()->update([
'subscription_id' => $subscription->transactionId,
'plan_id' => $plan->id,
'subscribed_at' => now()
]);
return redirect()->route('dashboard')->with('success', 'Subscription activated!');
}
return back()->withErrors(['subscription' => $subscription->message]);
}
}
Invoice Payment System
class InvoiceController extends Controller
{
public function payInvoice(Invoice $invoice, Request $request)
{
if ($invoice->isPaid()) {
return redirect()->back()->with('info', 'Invoice already paid');
}
$payment = SlickPaymentGateway::processPayment(
PaymentRequest::make([
'payment_method_id' => $request->payment_method_id,
'amount' => $invoice->total_amount,
'currency' => $invoice->currency,
'description' => "Invoice #{$invoice->number}",
'invoice_id' => $invoice->id,
'metadata' => [
'invoice_number' => $invoice->number,
'customer_id' => $invoice->customer_id
]
])
);
if ($payment->success) {
$invoice->update([
'status' => 'paid',
'paid_at' => now(),
'payment_transaction_id' => $payment->transactionId
]);
// Send payment confirmation
Mail::to($invoice->customer)->send(new PaymentReceiptMail($invoice, $payment));
return redirect()->route('invoice.show', $invoice)
->with('success', 'Payment processed successfully');
}
return back()->withErrors(['payment' => $payment->message]);
}
}
๐ Monitoring & Analytics
Payment Metrics
use Itul\SlickPaymentGateway\Models\SlickPayment;
use Illuminate\Support\Facades\DB;
class PaymentAnalytics
{
public function getDashboardMetrics()
{
$today = now()->startOfDay();
$lastMonth = now()->subMonth();
return [
'today_revenue' => SlickPayment::where('status', 'completed')
->whereDate('created_at', $today)
->sum('amount'),
'monthly_revenue' => SlickPayment::where('status', 'completed')
->where('created_at', '>=', $lastMonth)
->sum('amount'),
'success_rate' => $this->getSuccessRate(),
'top_gateways' => SlickPayment::select('gateway', DB::raw('count(*) as count'))
->where('created_at', '>=', $lastMonth)
->groupBy('gateway')
->orderBy('count', 'desc')
->get(),
'failed_payments' => SlickPayment::where('status', 'failed')
->whereDate('created_at', $today)
->count()
];
}
private function getSuccessRate(): float
{
$total = SlickPayment::whereDate('created_at', now())->count();
$successful = SlickPayment::where('status', 'completed')
->whereDate('created_at', now())
->count();
return $total > 0 ? ($successful / $total) * 100 : 0;
}
}
Custom Analytics Dashboard
// Create analytics endpoints
Route::middleware(['auth', 'slick.auth'])->prefix('analytics')->group(function () {
Route::get('/payments', [AnalyticsController::class, 'payments']);
Route::get('/revenue', [AnalyticsController::class, 'revenue']);
Route::get('/gateways', [AnalyticsController::class, 'gatewayPerformance']);
});
class AnalyticsController extends Controller
{
public function payments(Request $request)
{
$query = SlickPayment::query();
// Date filtering
if ($request->has(['start_date', 'end_date'])) {
$query->whereBetween('created_at', [
$request->start_date,
$request->end_date
]);
}
// Gateway filtering
if ($request->has('gateway')) {
$query->where('gateway', $request->gateway);
}
return response()->json([
'payments' => $query->with(['paymentMethod', 'owner'])->paginate(50),
'summary' => [
'total_amount' => $query->sum('amount'),
'average_amount' => $query->avg('amount'),
'payment_count' => $query->count()
]
]);
}
}
๐ง Artisan Commands
Available Commands
# Clean up expired payment methods
php artisan slick:clean-expired-payment-methods
# Generate PCI compliance report
php artisan slick:pci-audit-report
# Test gateway connections
php artisan slick:test-gateways
# Sync webhook configurations
php artisan slick:sync-webhooks
# Generate sample test data
php artisan slick:seed-test-data
Custom Command Examples
// Create custom commands for your business logic
class ProcessFailedPaymentsCommand extends Command
{
protected $signature = 'payments:retry-failed {--days=3 : Days to look back}';
protected $description = 'Retry failed payments from recent days';
public function handle()
{
$days = $this->option('days');
$cutoff = now()->subDays($days);
$failedPayments = SlickPayment::where('status', 'failed')
->where('created_at', '>=', $cutoff)
->where('retry_count', '<', 3)
->get();
$this->info("Found {$failedPayments->count()} failed payments to retry");
foreach ($failedPayments as $payment) {
$this->retryPayment($payment);
}
}
}
๐ Performance Optimization
Database Optimization
// Add indexes for performance
Schema::table('slick_payments', function (Blueprint $table) {
$table->index(['status', 'created_at']);
$table->index(['gateway', 'gateway_transaction_id']);
$table->index(['owner_type', 'owner_id']);
});
// Optimize queries with eager loading
$payments = SlickPayment::with([
'paymentMethod:id,last_four,brand',
'billingContact:id,name,email',
'owner:id,name'
])->where('status', 'completed')
->orderBy('created_at', 'desc')
->paginate(25);
Caching Strategy
use Illuminate\Support\Facades\Cache;
class PaymentService
{
public function getGatewayConfig(string $gateway): array
{
return Cache::remember(
"payment_gateway_config_{$gateway}",
now()->addHours(1),
fn() => config("slick-payment-gateway.drivers.{$gateway}")
);
}
public function getUserPaymentMethods(User $user): Collection
{
return Cache::tags(['user_payment_methods', "user_{$user->id}"])
->remember(
"user_payment_methods_{$user->id}",
now()->addMinutes(30),
fn() => $user->slickPaymentMethods()->active()->get()
);
}
}
Async Processing
use Itul\SlickPaymentGateway\Jobs\ProcessWebhookJob;
use Itul\SlickPaymentGateway\Jobs\SendPaymentNotificationJob;
// Queue heavy operations
class PaymentController extends Controller
{
public function processPayment(Request $request)
{
$payment = SlickPaymentGateway::processPayment($paymentRequest);
if ($payment->success) {
// Queue notification instead of sending immediately
SendPaymentNotificationJob::dispatch($payment)->delay(now()->addMinutes(1));
// Queue analytics update
UpdateAnalyticsJob::dispatch($payment);
}
return response()->json($payment);
}
}
๐ Multi-Language Support
Localization Setup
// resources/lang/en/slick-payment-gateway.php
return [
'payment' => [
'success' => 'Payment processed successfully',
'failed' => 'Payment failed: :reason',
'pending' => 'Payment is being processed',
'cancelled' => 'Payment was cancelled'
],
'errors' => [
'invalid_payment_method' => 'Invalid payment method',
'insufficient_funds' => 'Insufficient funds',
'expired_card' => 'Payment method has expired',
'gateway_error' => 'Payment gateway error'
]
];
// Usage in your application
echo __('slick-payment-gateway::payment.success');
Multi-Currency Support
// Configure currencies per gateway
'drivers' => [
'stripe' => [
'supported_currencies' => ['USD', 'EUR', 'GBP', 'CAD', 'AUD'],
'default_currency' => 'USD'
],
'paypal' => [
'supported_currencies' => ['USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY'],
'default_currency' => 'USD'
]
];
// Currency conversion helper
class CurrencyService
{
public function convertAmount(float $amount, string $from, string $to): float
{
if ($from === $to) return $amount;
$rate = $this->getExchangeRate($from, $to);
return round($amount * $rate, 2);
}
}
๐ฑ API Integration
RESTful API Endpoints
// API Routes (protected by Sanctum/Passport)
Route::middleware(['auth:sanctum'])->prefix('api/v1/payments')->group(function () {
// Payment methods
Route::get('/payment-methods', [ApiPaymentController::class, 'getPaymentMethods']);
Route::post('/payment-methods', [ApiPaymentController::class, 'storePaymentMethod']);
Route::delete('/payment-methods/{id}', [ApiPaymentController::class, 'deletePaymentMethod']);
// Payments
Route::post('/process', [ApiPaymentController::class, 'processPayment']);
Route::post('/refund', [ApiPaymentController::class, 'refundPayment']);
// Subscriptions
Route::post('/subscriptions', [ApiPaymentController::class, 'createSubscription']);
Route::delete('/subscriptions/{id}', [ApiPaymentController::class, 'cancelSubscription']);
});
API Controller Example
class ApiPaymentController extends Controller
{
public function processPayment(Request $request)
{
$request->validate([
'payment_method_id' => 'required|exists:slick_payment_methods,id',
'amount' => 'required|numeric|min:0.01|max:999999.99',
'currency' => 'required|string|size:3',
'description' => 'nullable|string|max:255'
]);
try {
$payment = SlickPaymentGateway::processPayment(
PaymentRequest::make($request->all())
);
return response()->json([
'success' => $payment->success,
'transaction_id' => $payment->transactionId,
'status' => $payment->status,
'message' => $payment->message
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Payment processing error'
], 500);
}
}
}
Mobile SDK Integration
// React Native / Flutter example
const processPayment = async (paymentData) => {
try {
const response = await fetch('/api/v1/payments/process', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}`
},
body: JSON.stringify(paymentData)
});
const result = await response.json();
if (result.success) {
showSuccessMessage('Payment processed successfully!');
navigateToSuccessScreen(result.transaction_id);
} else {
showErrorMessage(result.message);
}
} catch (error) {
showErrorMessage('Network error occurred');
}
};
๐ Troubleshooting
Common Issues & Solutions
1. Payment Method Token Validation Fails
// Problem: Token format validation fails
// Solution: Ensure client-side tokenization is working
// Check token format
if (!str_starts_with($token, 'pm_')) { // Stripe
throw new InvalidArgumentException('Invalid Stripe payment method token');
}
if (!str_starts_with($token, 'PAYID-')) { // PayPal
throw new InvalidArgumentException('Invalid PayPal payment method token');
}
2. Multi-Tenant Data Isolation Issues
// Problem: Cross-tenant data access
// Solution: Verify tenant scoping
// Check current tenant
$currentTenant = SlickTenantConnection::current();
if (!$currentTenant) {
throw new \Exception('No tenant context available');
}
// Verify model uses SassAware trait
class User extends Model
{
use \Itul\SlickPaymentGateway\Traits\SassAware; // Required for tenant scoping
}
3. Webhook Signature Verification Fails
// Problem: Webhooks fail validation
// Solution: Check webhook secrets and payload format
// Debug webhook signature
Log::debug('Webhook signature debug', [
'received_signature' => $request->header('Stripe-Signature'),
'calculated_signature' => $this->calculateExpectedSignature($payload),
'webhook_secret' => substr(config('slick-payment-gateway.drivers.stripe.webhook_secret'), 0, 10) . '...'
]);
4. Database Migration Issues
# Problem: Migration conflicts
# Solution: Clear and re-run migrations
php artisan migrate:rollback --path=/database/migrations/*slick*
php artisan migrate --path=/database/migrations/*slick*
# Or use package migrations specifically
php artisan migrate --path=vendor/itul/slick-payment-gateway/database/migrations
5. Rate Limiting Issues
// Problem: Too many requests error
// Solution: Adjust rate limiting configuration
// In config/slick-payment-gateway.php
'security' => [
'rate_limiting' => [
'payments' => 120, // Increase from 60 to 120 per minute
'webhooks' => 2000, // Increase from 1000 to 2000 per minute
],
],
Debug Mode
// Enable detailed logging for debugging
// In .env
SLICK_DEBUG_MODE=true
LOG_LEVEL=debug
// Check logs
tail -f storage/logs/laravel.log | grep slick
๐ Additional Resources
Official Documentation
Payment Gateway Documentation
๐ License
The MIT License (MIT). Please see License File for more information.
๐ Credits
Slick Payment Gateway is created and maintained by iTul.
Core Team
- Brandon Moore - Lead Developer & Architect
- Contributors - See Contributors
๐ What's Next?
Roadmap
- v2.1: GraphQL API support
- v2.2: Apple Pay & Google Pay integration
- v2.3: Advanced fraud detection
- v3.0: Blockchain payment support