Skip to content

How to Create a Custom Payment Gateway Module - Complete Tutorial

What You'll Learn

By the end of this guide, you'll understand how to integrate any payment provider into our Laravel TALL stack WhatsApp SaaS application with proper admin controls and tenant billing support.

Hey there! I'm going to walk you through creating a custom payment gateway module for our Laravel TALL stack WhatsApp SaaS application. This tutorial will show you exactly how to build something like a "TapGateway" module from scratch.

System Requirements

  • WhatsmarkSaaS v1.3.8+: Minimum version required for custom payment gateway development

What We're Building

Project Overview

Let's say you want to integrate a payment gateway called "TapGateway" into our application. I'll show you the complete process from start to finish, explaining each file and its purpose as we go.

By the end of this guide, you'll have:

  • A settings migration that stores your gateway configuration in the database
  • Dynamic properties that extend our core PaymentSettings class
  • A visual card in the main admin payment settings page
  • A complete settings configuration page
  • Backend logic to save and validate settings

Step 1: Build the Database Foundation

Important Security Note

Payment gateway settings contain sensitive API keys and secrets. Always use proper validation and encryption for these values.

Now we need to create settings for your payment gateway. This is where admins will configure API keys, enable/disable the gateway, etc.

Create the Settings Migration

bash
php artisan module:make-migration create_tap_gateway_settings TapGateway
php
<?php

use Spatie\LaravelSettings\Migrations\SettingsMigration;

return new class extends SettingsMigration
{
    protected array $settings = [
        // Basic gateway controls
        'payment.tap_gateway_enabled' => false, 

        // API configuration - these are what most payment gateways need
        'payment.tap_gateway_api_key' => '', 
        'payment.tap_gateway_secret_key' => '', 

        // Environment settings
        'payment.tap_gateway_sandbox_mode' => true, 

        // Webhook configuration for payment confirmations
        'payment.tap_gateway_webhook_secret' => '', 

        // Payment preferences
        'payment.tap_gateway_currency' => 'USD', 
        'payment.tap_gateway_description' => 'Payment via Tap Gateway', 
    ];

    public function up(): void
    {
        foreach ($this->settings as $key => $value) {
            if (! $this->migrator->exists($key)) {
                $this->migrator->add($key, $value);
            }
        }
    }

    public function down(): void
    {
        foreach (array_keys($this->settings) as $key) {
            if ($this->migrator->exists($key)) {
                $this->migrator->delete($key);
            }
        }
    }
};

What Each Setting Does

  • tap_gateway_enabled: Master switch - turns your gateway on/off completely
  • tap_gateway_api_key: Usually the public key for frontend operations
  • tap_gateway_secret_key: Private key for backend API calls - keep this secure!
  • tap_gateway_sandbox_mode: Toggles between test and live environments
  • tap_gateway_webhook_secret: Used to verify webhook authenticity
  • tap_gateway_currency: Default currency for transactions
  • tap_gateway_description: What customers see during checkout

Run the Settings Migration

bash
php artisan migrate --path=Modules/TapGateway/Database/Migrations
php artisan migrate --path=Modules/TapGateway/Database/Settings

Expected Output

text
Migrating: 2025_01_20_120000_create_tap_gateway_payment_settings

Step 2: Extend the Settings System with Dynamic Properties

Architecture Insight

Our application uses dynamic settings, which means we can extend the main PaymentSettings class without modifying core files. This is really clever architecture!

Here's where our architecture gets really clever. Our application uses dynamic settings, which means we can extend the main PaymentSettings class without modifying core files.

Create the Settings Extension Listener

bash
php artisan module:make-listener ExtendPaymentSettings TapGateway
php
<?php

namespace Modules\TapGateway\Listeners;

use App\Events\PaymentSettingsExtending;
use Nwidart\Modules\Facades\Module;

class ExtendPaymentSettings
{
    public function handle(PaymentSettingsExtending $event): void
    {
        // Only add extensions if our module is active
        if (! $this->isModuleActive()) {
            return;
        }

        // Add your payment gateway settings extensions
        // These become properties on the PaymentSettings class
        $event->addExtension('tap_gateway_enabled', false); 
        $event->addExtension('tap_gateway_api_key', ''); 
        $event->addExtension('tap_gateway_secret_key', ''); 
        $event->addExtension('tap_gateway_sandbox_mode', true); 
        $event->addExtension('tap_gateway_webhook_secret', ''); 
        $event->addExtension('tap_gateway_currency', 'USD'); 
        $event->addExtension('tap_gateway_description', 'Payment via Tap Gateway'); 
    }

    private function isModuleActive(): bool
    {
        return Module::find('TapGateway')?->isEnabled() ?? false;
    }
}

How Dynamic Properties Work

This is pretty cool - when this listener runs, it adds properties to our PaymentSettings class at runtime. So elsewhere in your code, you can do:

php
$settings = app(PaymentSettings::class);
echo $settings->tap_gateway_enabled; // This property now exists!

Step 3: Create the Admin Settings Card

Now let's build the visual component that appears in the main admin payment settings page. This card gives admins a quick overview and easy access to your gateway configuration.

Create the Card View

blade
<!-- Tap Gateway Payment Card -->
<a href="{{ route('admin.settings.payment.tap-gateway') }}" class="group relative">
    <div class="block p-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:border-info-500 dark:hover:border-info-500 transition-all duration-200 shadow-sm hover:shadow-md">
        <div class="flex items-center">
            <div class="shrink-0">
                <div class="relative">
                    <!-- Gateway Icon Container -->
                    <div class="w-12 h-12 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
                        <svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
                        </svg>
                    </div>
                    <!-- Status Indicator Dot -->
                    <span class="absolute -top-1 -right-1 h-3 w-3 rounded-full border-2 border-white dark:border-gray-800 {{ $statusClass }}"></span>
                </div>
            </div>
            <div class="ml-4 flex-1">
                <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
                    {{ t('tap_gateway') }}
                </h3>
                <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
                    {{ t('tap_gateway_description') }}
                </p>
            </div>
        </div>

        <!-- Status Section -->
        <div class="mt-2 border-t border-gray-200 dark:border-gray-700 pt-2">
            @if ($gatewayEnabled)
                <span class="inline-flex items-center text-xs font-medium text-success-600 dark:text-success-400">
                    <span class="w-2 h-2 rounded-full bg-success-400 mr-2"></span>
                    {{ t('active') }}
                </span>
            @else
                <span class="inline-flex items-center text-xs font-medium text-gray-500 dark:text-gray-400">
                    <span class="w-2 h-2 rounded-full bg-gray-300 dark:bg-gray-600 mr-2"></span>
                    {{ t('not_configured') }}
                </span>
            @endif
        </div>
    </div>
</a>
Card Components Breakdown
  • Icon Container: The blue rounded box with the credit card icon - make this your brand colors
  • Status Indicator: The small colored dot that shows active/inactive status
  • Gateway Info: Title and description of your payment gateway
  • Status Text: Clear text showing whether it's configured or not
  • Hover Effects: Smooth animations when admins hover over the card

Create the Card Display Listener

bash
php artisan module:make-listener AddTapGatewayPaymentSettings TapGateway
php
<?php

namespace Modules\TapGateway\Listeners;

use App\Events\PaymentSettingsViewRendering;
use App\Settings\PaymentSettings;
use Nwidart\Modules\Facades\Module;

class AddTapGatewayPaymentSettings
{
    public function handle(PaymentSettingsViewRendering $event): void
    {
        // Check if module is active
        if (! $this->isModuleActive()) { 
            return; 
        } 

        // Get payment settings with our dynamic properties
        $paymentSettings = app(PaymentSettings::class);
        $gatewayEnabled = $paymentSettings->tap_gateway_enabled ?? false; 

        // Generate the card HTML and add it to the admin interface
        $cardHtml = $this->getTapGatewayCard($gatewayEnabled); 
        $event->addPaymentGateway($cardHtml); 
    }

    private function getTapGatewayCard(bool $gatewayEnabled): string
    {
        $statusClass = $tapEnabled ? 'bg-success-400' : 'bg-gray-200'; 
        $statusText = $tapEnabled ? 'active' : 'not_configured'; 
        $statusTextClass = $tapEnabled 
            ? 'text-success-600 dark:text-success-400'
            : 'text-gray-600 dark:text-gray-400'; 

        return view('TapGateway::partials.payment-settings-card', [ 
            'tapEnabled' => $tapEnabled, 
            'statusClass' => $statusClass, 
            'statusText' => $statusText, 
            'statusTextClass' => $statusTextClass, 
        ])->render(); 
    }

    private function isModuleActive(): bool
    {
        return Module::find('TapGateway')?->isEnabled() ?? false;
    }
}

How the Event System Works

This is really elegant - the admin payment settings page fires a PaymentSettingsViewRendering event, and any module can listen for this event to add their own cards. Your card will automatically appear alongside Stripe, PayPal, and any other payment gateways.

Step 4: Build the Complete Settings Configuration Page

Security First

This page handles sensitive payment credentials. Always use HTTPS in production and implement proper input validation.

Now for the main event - the actual settings page where admins configure your gateway. This page needs to be intuitive, secure, and handle all the edge cases.

Create the Admin Controller

bash
php artisan module:make-controller Admin/TapGatewaySettingsController TapGateway
php
<?php

namespace Modules\TapGateway\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\PurifiedInput;
use App\Settings\PaymentSettings;
use Illuminate\Http\Request;
use Nwidart\Modules\Facades\Module;

class TapGatewaySettingsController extends Controller
{
    public function index() 
    {
        // Security check - make sure module is active
        if (!Module::find('TapGateway')?->isEnabled()) { 
            return redirect()->route('admin.settings') 
                ->with('error', t('module_not_available')); 
        } 

        // Get current settings to populate the form
        $settings = app(PaymentSettings::class);

        return view('tapgateway::admin.settings.tap-gateway', compact('settings'));
    }

    public function update(Request $request) 
    {
        // Validate the input with security in mind
        $request->validate([ 
            'gateway_enabled' => 'boolean', 
            'api_key' => [new PurifiedInput()], // Prevents XSS
            'secret_key' => [new PurifiedInput()], 
            'sandbox_mode' => 'boolean', 
            'webhook_secret' => [new PurifiedInput()], 
            'currency' => 'string|max:3', 
            'description' => [new PurifiedInput(), 'max:255'], 
        ]); 

        $settings = app(PaymentSettings::class);

        // Update each setting from the form
        $settings->tap_gateway_enabled = $request->boolean('gateway_enabled'); 
        $settings->tap_gateway_api_key = $request->input('api_key', ''); 
        $settings->tap_gateway_secret_key = $request->input('secret_key', ''); 
        $settings->tap_gateway_sandbox_mode = $request->boolean('sandbox_mode'); 
        $settings->tap_gateway_webhook_secret = $request->input('webhook_secret', ''); 
        $settings->tap_gateway_currency = $request->input('currency', 'USD'); 
        $settings->tap_gateway_description = $request->input('description', 'Payment via Tap Gateway'); 

        // Save all changes at once
        $settings->save();

        // Clear tenant cache so changes take effect immediately
        clear_cache();

        // Log this change for security/audit purposes
        whatsapp_log('Tap Gateway settings updated', 'info', [
            'gateway' => 'tap_gateway',
            'enabled' => $request->boolean('gateway_enabled'),
            'sandbox_mode' => $request->boolean('sandbox_mode'),
            'admin_id' => auth()->id(),
        ]);

        return redirect()->back()->with('success', t('settings_updated_successfully'));
    }
}

Step 5: Wire Everything Together

Don't forget to register all your event listeners in the service provider - this is crucial for everything to work together!

Now we need to register all our listeners and routes. Update your service provider to handle all the events:

php
<?php

namespace Modules\TapGateway\Providers;

use App\Events\PaymentSettingsExtending;
use App\Events\PaymentSettingsViewRendering;
use Illuminate\Support\ServiceProvider;
use Modules\TapGateway\Listeners\AddTapGatewayPaymentSettings;
use Modules\TapGateway\Listeners\ExtendPaymentSettings;

class TapGatewayServiceProvider extends ServiceProvider
{
    protected $moduleName = 'TapGateway';

    public function boot()
    {
        $this->registerTranslations();
        $this->registerConfig();
        $this->registerViews();
        $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations'));
        $this->registerRoutes();
        $this->registerEventListeners(); // This is crucial!
    }

    protected function registerEventListeners() 
    {
        // Register listener for admin card display
        $this->app['events']->listen( 
            PaymentSettingsViewRendering::class, 
            AddTapGatewayPaymentSettings::class
        ); 

        // Register listener for dynamic settings extension
        $this->app['events']->listen( 
            PaymentSettingsExtending::class, 
            ExtendPaymentSettings::class
        ); 
    }

    // ... other standard methods
}
php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Middleware\AdminMiddleware;
use App\Http\Middleware\SanitizeInputs;
use Modules\TapGateway\Http\Controllers\Admin\TapGatewaySettingsController;

// Admin routes for payment gateway configuration
Route::middleware(['web', 'auth', AdminMiddleware::class, SanitizeInputs::class])->prefix('admin')->name('admin.')->group(function () { 
    Route::prefix('/settings/payment')->name('settings.payment.')->group(function () { 
        Route::get('/tap-gateway', [TapGatewaySettingsController::class, 'index'])->name('tap-gateway'); 
        Route::patch('/tap-gateway', [TapGatewaySettingsController::class, 'update'])->name('tap-gateway.update'); 
    }); 
});

Step 6: Add Language Support

Internationalization

Always prepare your modules for multiple languages from the start. It's much easier than retrofitting later!

Create your admin language file to support internationalization:

json
{
    "tap_gateway": "Tap Gateway", 
    "tap_gateway_description": "Accept credit card payments through Tap Gateway", 
    "tap_gateway_settings": "Tap Gateway Settings", 
    "configure_tap_payments_description": "Configure Tap Gateway to accept online payments from your tenants",
    "enable_tap_gateway": "Enable Tap Gateway",
    "enable_tap_payments_description": "Allow tenants to pay their invoices using Tap Gateway",
    "api_key": "API Key", 
    "secret_key": "Secret Key", 
    "webhook_secret": "Webhook Secret", 
    "default_currency": "Default Currency",
    "sandbox_mode": "Sandbox Mode",
    "payment_description": "Payment Description",
    "api_key_description": "Your public API key from Tap Gateway dashboard",
    "secret_key_description": "Your private secret key - keep this secure!",
    "webhook_secret_description": "Secret used to verify webhook signatures from Tap Gateway", 
    "payment_description_help": "This text appears to customers during checkout", 
    "changes_apply_immediately": "Changes will apply to all new transactions immediately", 
    "settings_updated_successfully": "Tap Gateway settings have been updated successfully",
    "module_not_available": "This module is not currently available",
    "back": "Back",
    "save_changes": "Save Changes",
    "active": "Active",
    "not_configured": "Not Configured"
}

Complete Command Reference

All Artisan Commands Used

Here's a quick reference of all the Artisan commands we used:

bash
# 1. Create the module
php artisan module:make TapGateway --type=custom

# 2. Create listeners
php artisan module:make-listener ExtendPaymentSettings TapGateway
php artisan module:make-listener AddTapGatewayPaymentSettings TapGateway

# 3. Create admin controller
php artisan module:make-controller Admin/TapGatewaySettingsController TapGateway

# 4. Create services (for next steps)
php artisan module:make-class Services/TapPaymentGateway TapGateway

# 5. Optional components
php artisan module:make-request TapGatewaySettingsRequest TapGateway
php artisan module:make-job ProcessTapGatewayPayment TapGateway
php artisan module:make-event TapGatewayPaymentProcessed TapGateway
php artisan module:make-test TapGatewayTest TapGateway

Testing Your Admin Configuration

Testing Checklist

Before moving on to tenant billing, test your admin interface:

  1. Access the Admin Payment Settings:

    • Go to your admin panel
    • Navigate to Settings → Payment Settings
    • You should see your Tap Gateway card
  2. Configure Your Gateway:

    • Click on the Tap Gateway card
    • Fill in test API credentials
    • Enable sandbox mode
    • Save the settings
  3. Verify Everything Works:

    • Check that the card shows "Active" status
    • Verify settings are saved correctly
    • Test form validation by leaving required fields empty

Troubleshooting Common Issues

Card Not Appearing

If your payment gateway card isn't showing up:

bash
# Check if module is activated
php artisan module:list

# Clear cache
php artisan cache:clear

# Check event listener registration in service provider

Settings Not Saving

If settings aren't persisting:

bash
# Run the settings migration
php artisan migrate --path=Modules/TapGateway/Database/Settings

# Check database for settings table
# Verify PaymentSettingsExtending event is being fired

What You've Built

Achievement Unlocked!

Congratulations! You now have a complete admin configuration system that includes:

  • Secure Settings Storage: Your gateway settings are stored safely in the database
  • Dynamic Properties: Settings automatically extend the PaymentSettings class
  • Visual Admin Interface: A beautiful card in the main payment settings page
  • Complete Configuration Page: All settings with proper validation and help text
  • Security Features: Input validation, XSS protection, and audit logging
  • User Experience: Intuitive interface with clear feedback and error handling

Your payment gateway now integrates seamlessly with the admin interface, and administrators can easily configure it without touching any code. The settings you configure here will be used by the tenant billing system we'll cover next.


Next Steps

Ready to let your customers actually use this payment gateway? Head over to our Tenant Billing Integration Guide to complete the customer-facing implementation.

© 2024 - Corbital Technologies. All rights reserved.