Overview
Eufaturo Billing provides a unified interface for working with multiple payment gateways. Whether you’re using Stripe, PayPal, Eupago, or building your own integration, the API remains consistent across all providers.
All gateways implement the same PaymentGatewayInterface, allowing you to switch providers or support multiple gateways simultaneously without changing your code.
How Gateways Work
Gateway Registration
Gateways register themselves automatically through Laravel package discovery:
// In gateway package's service provider
class StripeServiceProvider extends ServiceProvider
{
public function boot(): void
{
$gatewayManager = app(GatewayManager::class);
$gatewayManager->register(StripeGateway::class);
}
}
Once registered, gateways are available through the Billing facade:
use Eufaturo\Billing\Billing;
// Get a gateway
$stripe = Billing::gateways()->get('stripe');
// Check if gateway exists
if (Billing::gateways()->has('paypal')) {
$paypal = Billing::gateways()->get('paypal');
}
// Get all registered gateways
$allGateways = Billing::gateways()->all();
Gateway Manager
The GatewayManager handles gateway registration and retrieval:
use Eufaturo\Billing\PaymentGateways\GatewayManager;
$manager = app(GatewayManager::class);
// Register a gateway
$manager->register(StripeGateway::class);
// Get a gateway (throws if not found)
$gateway = $manager->get('stripe');
// Check availability
if ($manager->has('stripe')) {
// Gateway is available
}
// Get all gateways
$gateways = $manager->all(); // ['stripe' => StripeGateway, ...]
Gateway Interface
All gateways implement PaymentGatewayInterface with these capabilities:
1. Identification
$gateway->getIdentifier(); // 'stripe', 'paypal', etc.
$gateway->getName(); // 'Stripe', 'PayPal', etc.
2. Customer Management
// Create or retrieve customer
$customer = $gateway->createOrRetrieveCustomer($billable);
// Returns CustomerDto with:
// - external_id: Gateway customer ID
// - email: Customer email
// - name: Customer name
3. Subscription Checkout
// Create checkout session
$checkout = $gateway->createSubscriptionCheckout(
billable: $user,
plan: $plan,
successUrl: route('checkout.success'),
cancelUrl: route('checkout.cancel'),
metadata: ['user_id' => $user->id],
);
// Redirect to gateway checkout
return redirect($checkout->url);
// After checkout, retrieve session
$session = $gateway->retrieveSubscriptionCheckout($sessionId);
4. One-Time Payments
// Create payment checkout
$checkout = $gateway->createPaymentCheckout(
billable: $user,
order: $order,
successUrl: route('payment.success'),
cancelUrl: route('payment.cancel'),
);
return redirect($checkout->url);
// Retrieve payment status
$payment = $gateway->retrievePaymentCheckout($sessionId);
5. Subscription Management
// Retrieve subscription
$subscription = $gateway->retrieveSubscription($externalId);
// Cancel subscription
$gateway->cancelSubscription($externalId, immediately: true);
// Resume subscription
$gateway->resumeSubscription($externalId);
// Update payment method
$gateway->updatePaymentMethod($externalId, $paymentMethodId);
// Update plan
$gateway->updatePlan($externalId, $newPlan, prorate: true);
6. Refunds
// Full refund
$refund = $gateway->refundPayment($paymentId, $amount);
// Partial refund
$refund = $gateway->refundPayment($paymentId, 5000); // $50.00
7. Feature Detection
// Check if gateway supports a feature
if ($gateway->supports('checkout')) {
// Gateway has hosted checkout
}
if ($gateway->supports('refunds')) {
// Gateway supports refunds
}
// Common features:
// - 'checkout': Hosted checkout
// - 'one_time_payment': One-time payments
// - 'refunds': Payment refunds
// - 'direct_creation': Direct API subscription creation
// - 'resume': Resume cancelled subscriptions
// - 'payment_method_update': Update payment methods
// - 'webhooks': Webhook support
8. Frontend Integration
// Check if JavaScript is required
if ($gateway->requiresJavaScript('card')) {
$script = $gateway->getJavaScriptSnippet('card');
// Include script in page
}
// Get initialization payload for frontend
$payload = $gateway->getJavaScriptInitializationPayload(
paymentMethodCode: 'card',
currency: 'eur',
amount: 9900, // €99.00
);
Available Gateways
Official Gateway Packages
Coming Soon
- PayPal - PayPal subscriptions and payments
- Paddle - Merchant of record with global tax compliance
- Lemon Squeezy - All-in-one payment platform
Want to add support for another gateway? See Creating Custom Gateway.
Gateway Selection Strategies
1. Single Gateway (Simplest)
Use one gateway for all customers:
// In AppServiceProvider
use Eufaturo\Billing\Billing;
public function boot(): void
{
// Always use Stripe
$this->app->bind('default.gateway', function () {
return Billing::gateways()->get('stripe');
});
}
2. Regional Selection
Choose gateway based on customer location:
public function getGatewayForUser(User $user)
{
return match($user->country) {
'PT' => Billing::gateways()->get('eupago'), // Portugal
'BR' => Billing::gateways()->get('pagseguro'), // Brazil
'US' => Billing::gateways()->get('stripe'), // United States
default => Billing::gateways()->get('stripe'), // Default
};
}
3. Customer Choice
Let customers choose their preferred payment method:
// In checkout controller
public function showCheckout()
{
$gateways = Billing::gateways()->all();
return view('checkout', [
'gateways' => $gateways,
]);
}
{{-- In checkout view --}}
<form wire:submit="subscribe">
<label>Choose Payment Method</label>
<select wire:model="gatewayIdentifier">
@foreach($gateways as $identifier => $gateway)
<option value="{{ $identifier }}">
{{ $gateway->getName() }}
</option>
@endforeach
</select>
<button type="submit">Subscribe</button>
</form>
// In Livewire component
public function subscribe()
{
$gateway = Billing::gateways()->get($this->gatewayIdentifier);
$checkout = $gateway->createSubscriptionCheckout(
billable: $this->user,
plan: $this->plan,
successUrl: route('checkout.success'),
cancelUrl: route('checkout.cancel'),
);
return redirect($checkout->url);
}
4. Feature-Based Selection
Choose gateway based on required features:
public function getGatewayWithFeature(string $feature)
{
$gateways = Billing::gateways()->all();
foreach ($gateways as $gateway) {
if ($gateway->supports($feature)) {
return $gateway;
}
}
throw new Exception("No gateway supports '{$feature}'");
}
// Usage
$gateway = $this->getGatewayWithFeature('refunds');
Gateway Records
Gateway records link your local entities to external gateway IDs using polymorphic relationships:
// After creating a subscription
$subscription = Billing::subscriptions()->create(
billable: $user,
plan: $plan,
type: SubscriptionType::PaymentGatewayManaged,
paymentGateway: $stripe,
);
// Get Stripe subscription ID
$stripeRecord = $subscription->getGatewayRecordForGateway($stripe);
echo $stripeRecord->external_id; // "sub_1234567890"
// Or get external ID directly
$externalId = $subscription->getExternalIdForGateway($stripe);
Database Structure
// billing_gateway_records table
Schema::create('billing_gateway_records', function (Blueprint $table) {
$table->id();
$table->foreignId('payment_gateway_id');
$table->morphs('entity'); // entity_type, entity_id
$table->string('external_id');
$table->json('metadata')->nullable();
$table->timestamps();
});
This allows:
- One entity can have references to multiple gateways
- Clean separation between local and external data
- Easy migration between gateways
- Support for any entity type (subscriptions, plans, customers)
Installation
Installing Gateway Packages
# Install Stripe gateway
composer require eufaturo/billing-stripe
# Install Eupago gateway
composer require eufaturo/billing-eupago
Configuration
After installation, publish gateway configuration:
php artisan vendor:publish --tag=billing-stripe-config
php artisan vendor:publish --tag=billing-eupago-config
Configure API keys in .env:
# Stripe
STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Eupago
EUPAGO_API_KEY=your-api-key
EUPAGO_WEBHOOK_SECRET=your-webhook-secret
Webhooks
Gateways use webhooks to notify your application of events:
Registering Webhook Handlers
// In gateway package
$gatewayManager->registerWebhookHandler(StripeWebhookHandler::class);
Webhook Routes
Each gateway package registers its webhook route:
// routes/web.php (in gateway package)
Route::post('/webhooks/stripe', StripeWebhookController::class);
Route::post('/webhooks/eupago', EupagoWebhookController::class);
Webhook Events
Common webhook events:
- Subscription Created - New subscription activated
- Subscription Updated - Plan changed, payment method updated
- Subscription Cancelled - Customer cancelled subscription
- Payment Succeeded - Payment processed successfully
- Payment Failed - Payment attempt failed
- Invoice Paid - Invoice was paid
- Invoice Payment Failed - Invoice payment failed
Testing Gateways
Mock Gateway for Tests
use Eufaturo\Billing\PaymentGateways\Contracts\PaymentGatewayInterface;
test('can create subscription with any gateway', function () {
$mockGateway = Mockery::mock(PaymentGatewayInterface::class);
$mockGateway->shouldReceive('getIdentifier')->andReturn('test');
$mockGateway->shouldReceive('getName')->andReturn('Test Gateway');
$mockGateway->shouldReceive('createSubscriptionCheckout')
->andReturn(new CheckoutResponse(
url: 'https://test.com/checkout',
id: 'checkout_test123',
));
// Register mock
Billing::gateways()->register(get_class($mockGateway));
// Test your code
$subscription = Billing::subscriptions()->create(
billable: $user,
plan: $plan,
type: SubscriptionType::PaymentGatewayManaged,
paymentGateway: $mockGateway,
);
expect($subscription)->toBeInstanceOf(Subscription::class);
});
Test Mode
Most gateways provide test/sandbox modes:
# Use test keys
STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
Best Practices
✅ Do
-
Check gateway availability before using it
if (Billing::gateways()->has('stripe')) {
$stripe = Billing::gateways()->get('stripe');
}
-
Handle gateway exceptions gracefully
try {
$checkout = $gateway->createSubscriptionCheckout(...);
} catch (GatewayOperationFailedException $e) {
Log::error('Gateway error', ['error' => $e->getMessage()]);
return back()->with('error', 'Payment processing failed');
}
-
Store gateway records for all gateway-created entities
$subscription->gatewayRecords()->create([
'payment_gateway_id' => $gateway->id,
'external_id' => $externalSubscriptionId,
]);
-
Use feature detection before calling optional methods
if ($gateway->supports('refunds')) {
$gateway->refundPayment($paymentId, $amount);
}
❌ Don’t
-
Don’t hardcode gateway identifiers everywhere
// ❌ Bad
$gateway = Billing::gateways()->get('stripe'); // Hardcoded
// ✅ Good
$gateway = $this->getDefaultGateway(); // Configurable
-
Don’t skip webhook verification
// ❌ Bad - trusts any webhook
Route::post('/webhook', function (Request $request) {
$data = $request->all();
// Process without verification
});
// ✅ Good - verifies signature
Route::post('/webhook', StripeWebhookController::class);
-
Don’t call unsupported methods without checking
// ❌ Bad
$gateway->refundPayment($id, $amount); // Might not be supported
// ✅ Good
if ($gateway->supports('refunds')) {
$gateway->refundPayment($id, $amount);
}
Common Patterns
Dynamic Gateway Resolution
class GatewaySelector
{
public function selectForUser(User $user): PaymentGatewayInterface
{
// 1. Check user preference
if ($user->preferred_gateway) {
return Billing::gateways()->get($user->preferred_gateway);
}
// 2. Check regional
$regional = config("billing.regional_gateways.{$user->country}");
if ($regional && Billing::gateways()->has($regional)) {
return Billing::gateways()->get($regional);
}
// 3. Use default
return Billing::gateways()->get(config('billing.default_gateway'));
}
}
Fallback Gateway
public function createSubscription(User $user, ProductPlan $plan)
{
$primaryGateway = Billing::gateways()->get('stripe');
try {
return $primaryGateway->createSubscriptionCheckout(...);
} catch (GatewayOperationFailedException $e) {
// Fallback to alternative gateway
Log::warning('Primary gateway failed, trying fallback');
$fallbackGateway = Billing::gateways()->get('paypal');
return $fallbackGateway->createSubscriptionCheckout(...);
}
}
Troubleshooting
”Payment gateway ‘X’ is not registered”
Error:
PaymentGatewayNotFoundException: Payment gateway 'stripe' is not registered.
Solutions:
-
Install the gateway package:
composer require eufaturo/billing-stripe
-
Verify package is auto-discovered:
composer dump-autoload
php artisan package:discover
-
Check service provider is registered:
// config/app.php
'providers' => [
Eufaturo\BillingStripe\StripeServiceProvider::class,
],
Gateway Configuration Missing
Error:
InvalidArgumentException: Stripe API key not configured
Solution:
# Add to .env
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLIC_KEY=pk_test_...
Webhook Signature Verification Failed
Error:
WebhookVerificationException: Invalid signature
Solutions:
-
Verify webhook secret is configured:
STRIPE_WEBHOOK_SECRET=whsec_...
-
Check webhook URL is correct in gateway dashboard
-
Verify webhook endpoint is publicly accessible (not behind auth)
Next Steps
See Also