Skip to main content

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

Community Gateways

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:
  1. Install the gateway package:
    composer require eufaturo/billing-stripe
    
  2. Verify package is auto-discovered:
    composer dump-autoload
    php artisan package:discover
    
  3. 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:
  1. Verify webhook secret is configured:
    STRIPE_WEBHOOK_SECRET=whsec_...
    
  2. Check webhook URL is correct in gateway dashboard
  3. Verify webhook endpoint is publicly accessible (not behind auth)

Next Steps


See Also