Skip to main content

Overview

Eufaturo Billing is gateway-agnostic - your billing logic works the same whether you use Stripe, PayPal, Eupago, or any other payment provider. Switch gateways, use multiple simultaneously, or build custom integrations without changing your core code.
Gateway-agnostic means your business logic (products, subscriptions, features) is completely separated from payment processing. Change gateways anytime without rewriting code.

The Problem with Tight Coupling

Traditional Approach (Tightly Coupled)

// ❌ Tightly coupled to Stripe
use Stripe\StripeClient;

class SubscriptionController
{
    public function create()
    {
        $stripe = new StripeClient(config('stripe.secret'));

        // Create Stripe customer
        $customer = $stripe->customers->create([
            'email' => $user->email,
        ]);

        // Create Stripe subscription
        $subscription = $stripe->subscriptions->create([
            'customer' => $customer->id,
            'items' => [['price' => 'price_123']],
        ]);

        // Save to database
        DB::table('subscriptions')->insert([
            'user_id' => $user->id,
            'stripe_id' => $subscription->id,
        ]);
    }
}
Problems:
  • ❌ Stripe code everywhere
  • ❌ Can’t switch to PayPal without rewriting
  • ❌ Can’t test without Stripe
  • ❌ Can’t support multiple gateways
  • ❌ Vendor lock-in

Gateway-Agnostic Approach

// ✅ Gateway-agnostic
use Eufaturo\Billing\Billing;

class SubscriptionController
{
    public function create()
    {
        $subscription = Billing::subscriptions()->create(
            billable: $user,
            plan: $plan,
            type: SubscriptionType::PaymentGatewayManaged,
            paymentGateway: $gateway, // Any gateway!
        );

        // That's it! Gateway details handled internally
    }
}
Benefits:
  • ✅ No gateway-specific code
  • ✅ Switch gateways easily
  • ✅ Test without real gateways
  • ✅ Support multiple gateways
  • ✅ No vendor lock-in

How It Works

Architecture Layers

Your Application Code

Billing Core (Gateway-Agnostic)
├── Products
├── Plans
├── Subscriptions
├── Features

Gateway Abstraction Layer
├── PaymentGatewayInterface
├── GatewayManager
├── Gateway Records

Gateway Implementations
├── Stripe Gateway
├── PayPal Gateway
├── Eupago Gateway
├── Custom Gateway

The Gateway Interface

All gateways implement a common interface:
interface PaymentGatewayInterface
{
    public function getIdentifier(): string;
    public function getName(): string;

    // Subscription operations
    public function createSubscriptionCheckout(CreateSubscriptionCheckoutDto $dto): CheckoutResponse;
    public function cancelSubscription(string $externalId): void;
    public function resumeSubscription(string $externalId): void;

    // Payment operations
    public function createPaymentCheckout(CreatePaymentCheckoutDto $dto): CheckoutResponse;
    public function refundPayment(RefundDto $dto): RefundResponse;

    // Webhook handling
    public function handleWebhook(Request $request): void;
}
Key Point: Your code only interacts with the interface, never the specific implementation.

Using Gateways

Getting a Gateway

use Eufaturo\Billing\Billing;

// Get gateway by identifier
$stripe = Billing::gateways()->get('stripe');
$paypal = Billing::gateways()->get('paypal');

// Check if gateway exists
if (Billing::gateways()->has('stripe')) {
    $stripe = Billing::gateways()->get('stripe');
}

// Get all registered gateways
$gateways = Billing::gateways()->all();

Creating Subscriptions with Gateways

use Eufaturo\Billing\Billing;
use Eufaturo\Billing\Subscriptions\Enums\SubscriptionType;

// Get the gateway
$stripe = Billing::gateways()->get('stripe');

// Create subscription (gateway-managed)
$subscription = Billing::subscriptions()->create(
    billable: $user,
    plan: $plan,
    type: SubscriptionType::PaymentGatewayManaged,
    paymentGateway: $stripe, // Pass gateway instance
);

// Gateway automatically:
// ✓ Creates customer
// ✓ Creates subscription
// ✓ Stores references
// ✓ Sets up webhooks

Gateway vs Local Management

Two subscription types: Gateway handles billing cycle:
$subscription = Billing::subscriptions()->create(
    billable: $user,
    plan: $plan,
    type: SubscriptionType::PaymentGatewayManaged,
    paymentGateway: $gateway,
);
Gateway Handles:
  • Billing cycle (when to charge)
  • Proration (plan changes)
  • Payment collection
  • Retry logic
  • Invoice generation

2. Locally-Managed

You handle billing logic:
$subscription = Billing::subscriptions()->create(
    billable: $user,
    plan: $plan,
    type: SubscriptionType::LocallyManaged,
    // No gateway needed
);
You Handle:
  • Billing cycle logic
  • Manual proration
  • Triggering payments
  • Retry logic
  • Invoice generation
Use Gateway-Managed unless you have specific requirements for custom billing logic.

Gateway Records

Gateway records link your local data to external gateway IDs:
// After creating subscription
$subscription = Billing::subscriptions()->create(/* ... */);

// Get Stripe ID
$stripeGateway = Billing::gateways()->get('stripe');
$stripeRecord = $subscription->getGatewayRecordForGateway($stripeGateway);

echo $stripeRecord->external_id; // "sub_1234567890"

// Or get external ID directly
$externalId = $subscription->getExternalIdForGateway($stripeGateway);

How It Works

// billing_gateway_records table
Schema::create('billing_gateway_records', function (Blueprint $table) {
    $table->id();
    $table->foreignId('payment_gateway_id');
    $table->morphs('entity'); // subscription_id, plan_id, etc.
    $table->string('external_id'); // Stripe/PayPal ID
    $table->json('metadata')->nullable();
    $table->timestamps();
});
Example Records:
| entity_type  | entity_id | payment_gateway | external_id      |
|--------------|-----------|-----------------|------------------|
| Subscription | 1         | Stripe          | sub_abc123       |
| Subscription | 1         | PayPal          | I-XYZU2ABCD      |
| ProductPlan  | 5         | Stripe          | price_def456     |
Why Polymorphic?
  • Same record system for subscriptions, plans, customers, etc.
  • Clean database design
  • Support multiple gateways per entity

Switching Gateways

Scenario: Stripe → PayPal

Your existing code stays the same:
// This code doesn't change!
$subscription = Billing::subscriptions()->create(
    billable: $user,
    plan: $plan,
    type: SubscriptionType::PaymentGatewayManaged,
    paymentGateway: $gateway, // Just pass different gateway
);
Only change the gateway:
// Before
$gateway = Billing::gateways()->get('stripe');

// After
$gateway = Billing::gateways()->get('paypal');

Migration Strategy

  1. Install new gateway package:
composer require eufaturo/billing-paypal
  1. Configure gateway:
php artisan billing:gateway:install paypal
  1. Test in development:
// Test with new gateway
$paypal = Billing::gateways()->get('paypal');
$subscription = Billing::subscriptions()->create(
    billable: $testUser,
    plan: $plan,
    type: SubscriptionType::PaymentGatewayManaged,
    paymentGateway: $paypal,
);
  1. Gradual rollout:
// New users → PayPal
// Existing users → Stripe (until migrated)

if ($user->created_at > '2024-01-01') {
    $gateway = Billing::gateways()->get('paypal');
} else {
    $gateway = Billing::gateways()->get('stripe');
}

Supporting Multiple Gateways

Different Gateways per Region

// European customers → Stripe
// US customers → PayPal
// Portuguese customers → Eupago

$gateway = match($user->country) {
    'PT' => Billing::gateways()->get('eupago'),
    'US' => Billing::gateways()->get('paypal'),
    default => Billing::gateways()->get('stripe'),
};

$subscription = Billing::subscriptions()->create(
    billable: $user,
    plan: $plan,
    type: SubscriptionType::PaymentGatewayManaged,
    paymentGateway: $gateway,
);

Customer Choice

Let customers choose their payment method:
// In your checkout page
$availableGateways = Billing::gateways()->all();

return view('checkout', [
    'gateways' => $availableGateways,
]);
{{-- checkout.blade.php --}}
<form wire:submit="subscribe">
    <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);

    $subscription = Billing::subscriptions()->create(
        billable: $this->user,
        plan: $this->plan,
        type: SubscriptionType::PaymentGatewayManaged,
        paymentGateway: $gateway,
    );
}

Testing Without Real Gateways

Mock Gateway for Testing

// In tests
use Eufaturo\Billing\PaymentGateways\Contracts\PaymentGatewayInterface;

test('can create subscription', function () {
    // Mock gateway
    $mockGateway = Mockery::mock(PaymentGatewayInterface::class);
    $mockGateway->shouldReceive('getIdentifier')->andReturn('test');
    $mockGateway->shouldReceive('createSubscriptionCheckout')
        ->andReturn(new CheckoutResponse(
            checkoutUrl: 'https://test.com/checkout',
            externalId: 'sub_test123',
        ));

    // Register mock
    Billing::gateways()->register(get_class($mockGateway));

    // Test subscription creation
    $subscription = Billing::subscriptions()->create(
        billable: $user,
        plan: $plan,
        type: SubscriptionType::PaymentGatewayManaged,
        paymentGateway: $mockGateway,
    );

    expect($subscription)->toBeInstanceOf(Subscription::class);
});

Test Mode

Most gateways have test modes:
// .env.testing
STRIPE_SECRET=sk_test_...  // Test key
STRIPE_WEBHOOK_SECRET=whsec_test_...

Creating Custom Gateways

Step 1: Implement the Interface

namespace App\Billing\Gateways;

use Eufaturo\Billing\PaymentGateways\Contracts\PaymentGatewayInterface;

class CustomGateway implements PaymentGatewayInterface
{
    public function getIdentifier(): string
    {
        return 'custom';
    }

    public function getName(): string
    {
        return 'Custom Payment Gateway';
    }

    public function createSubscriptionCheckout(CreateSubscriptionCheckoutDto $dto): CheckoutResponse
    {
        // Your implementation
        $response = Http::post('https://api.custom.com/subscriptions', [
            'customer_email' => $dto->billable->getBillableEmail(),
            'plan_id' => $dto->plan->slug,
        ]);

        return new CheckoutResponse(
            checkoutUrl: $response['checkout_url'],
            externalId: $response['subscription_id'],
        );
    }

    // Implement other methods...
}

Step 2: Register Your Gateway

// In AppServiceProvider
use Eufaturo\Billing\PaymentGateways\GatewayManager;

public function boot(): void
{
    $gatewayManager = app(GatewayManager::class);
    $gatewayManager->register(CustomGateway::class);
}

Step 3: Use Your Gateway

$custom = Billing::gateways()->get('custom');

$subscription = Billing::subscriptions()->create(
    billable: $user,
    plan: $plan,
    type: SubscriptionType::PaymentGatewayManaged,
    paymentGateway: $custom,
);

Gateway Packages

Official Gateways

# Stripe
composer require eufaturo/billing-stripe

# Eupago (Portuguese payment methods)
composer require eufaturo/billing-eupago

# PayPal (coming soon)
composer require eufaturo/billing-paypal

Auto-Discovery

Gateways register automatically via 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);
    }
}

Benefits Summary

For Developers

  • Clean Code - No gateway-specific logic in your app
  • Easy Testing - Mock gateways for tests
  • Flexibility - Switch gateways anytime
  • Type Safety - All gateways implement same interface

For Businesses

  • No Vendor Lock-in - Not tied to one provider
  • Geographic Expansion - Use regional gateways
  • Cost Optimization - Switch to cheaper providers
  • Risk Mitigation - Backup gateway if primary fails

For Customers

  • Payment Choice - Multiple payment options
  • Local Methods - Regional payment methods
  • Better UX - Familiar checkout experience

Real-World Example

Multi-Gateway Implementation

// config/billing.php
return [
    'default_gateway' => env('BILLING_GATEWAY', 'stripe'),

    'regional_gateways' => [
        'PT' => 'eupago',  // Portugal
        'BR' => 'pagseguro', // Brazil
        'US' => 'stripe',  // United States
        'EU' => 'stripe',  // Europe
    ],
];
// app/Services/GatewaySelector.php
class GatewaySelector
{
    public function getGatewayForUser(User $user): PaymentGatewayInterface
    {
        $countryCode = $user->country;

        // Regional gateway
        $identifier = config("billing.regional_gateways.{$countryCode}")
            ?? config('billing.default_gateway');

        return Billing::gateways()->get($identifier);
    }
}
// In your controller
public function subscribe(GatewaySelector $selector)
{
    $gateway = $selector->getGatewayForUser($user);

    $subscription = Billing::subscriptions()->create(
        billable: $user,
        plan: $plan,
        type: SubscriptionType::PaymentGatewayManaged,
        paymentGateway: $gateway,
    );
}

Common Questions

Q: Can I use multiple gateways for one customer?

A: Yes! You can create subscriptions with different gateways:
// One customer, two gateways
$stripeSubscription = Billing::subscriptions()->create(
    billable: $user,
    plan: $proPlan,
    paymentGateway: Billing::gateways()->get('stripe'),
);

$paypalSubscription = Billing::subscriptions()->create(
    billable: $user,
    plan: $basicPlan,
    paymentGateway: Billing::gateways()->get('paypal'),
);

Q: What happens to existing subscriptions when I switch gateways?

A: Existing subscriptions continue with their original gateway. New subscriptions use the new gateway.

Q: Can I migrate subscriptions between gateways?

A: Yes, but it requires:
  1. Cancel old gateway subscription
  2. Create new gateway subscription
  3. Update gateway records
This is a manual process as it involves two external systems.

Next Steps


See Also