Tenant Billing Integration - Complete Step-by-Step Guide
What You'll Build
By the end of this guide, your tenants will be able to see your payment gateway in their billing details page, pay invoices using your gateway, set up auto-billing with your payment method, and have a complete checkout and payment flow.
Now that you've got your admin configuration working perfectly, let's tackle the tenant side of things. This is where your payment gateway actually comes to life for your customers - they'll see your gateway as an option when paying their invoices and setting up auto-billing.
I'm going to show you exactly how to integrate your payment gateway into the tenant billing system using our hook-based architecture. This approach is really clean because it doesn't require modifying any core files.
Architecture Overview
Our billing system uses a WordPress-style hook system that allows modules to extend functionality without touching core files. This is super powerful because your module can add features that integrate seamlessly.
Step 1: Hook Integration with the Billing System
Important Concept
The billing details page uses hooks that fire at specific points, allowing your module to participate in the billing flow without the core system knowing anything specific about your gateway.
Understanding the Hook System
The billing details page (app\Livewire\Tenant\TenantSubscription\BillingDetails.php
) fires these hooks:
// Hook 1: Settings Loading
billing_details.payment_settings_keys
// Let modules specify which settings to load
// Hook 2: Gateway Registration
billing_details.enabled_gateways
// Let modules register their gateways for tenant use
Hook Flow Diagram
Register Your Hooks
Hook Registration is Critical
If you don't register these hooks properly, your payment gateway won't appear in the tenant billing interface, even if it's enabled in admin.
Update your service provider to include hook registration:
<?php
namespace Modules\TapGateway\Providers;
use App\Events\PaymentGatewayRegistration;
use App\Events\PaymentSettingsExtending;
use App\Events\PaymentSettingsViewRendering;
use Illuminate\Support\ServiceProvider;
use Modules\TapGateway\Listeners\AddTapGatewayPaymentSettings;
use Modules\TapGateway\Listeners\ExtendPaymentSettings;
use Modules\TapGateway\Listeners\RegisterTapGateway;
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->registerHooks(); // Add this line!
}
protected function registerHooks()
{
// Hook 1: Tell the billing system which settings to load
add_filter('billing_details.payment_settings_keys', function ($settingsKeys) {
// Add your gateway's settings to the list that gets loaded
$settingsKeys[] = 'payment.tap_gateway_enabled';
return $settingsKeys;
});
// Hook 2: Register your gateway as available for tenant billing
add_filter('billing_details.enabled_gateways', function ($enabledGateways) {
// Get your gateway settings directly from the settings system
$gatewaySettings = get_batch_settings([
'payment.tap_gateway_enabled',
]);
// Only enable if admin has configured and enabled it
$gatewayEnabled = ($gatewaySettings['payment.tap_gateway_enabled'] ?? false);
// Add your gateway to the list of enabled gateways
$enabledGateways['tap_gateway'] = $gatewayEnabled;
return $enabledGateways;
});
}
// ... other methods stay the same
}
How the Hook System Works in Detail
Here's what happens when a tenant visits their billing page:
- Settings Loading: The
billing_details.payment_settings_keys
hook fires, and your module adds its settings to the load list - Gateway Registration: The
billing_details.enabled_gateways
hook fires, and your module checks if it should be available - UI Rendering: The billing page only shows gateways that are enabled and configured
- Payment Flow: When tenant selects your gateway, they're directed to your payment URLs
Pretty elegant, right? Your module participates in the billing system without the core system knowing anything specific about Tap Gateway.
Step 2: Create Tenant-Specific Language Support
Language Separation
Tenants need their own translations, separate from admin translations. This is important for multi-language support and keeping admin and tenant interfaces distinct.
Create Tenant Language File
Create the file: Modules/TapGateway/resources/lang/tenant_en.json
{
"pay_with_tap_gateway": "Pay with Tap Gateway",
"tap_gateway": "Tap Gateway",
"tap_gateway_payment": "Tap Gateway Payment",
"setup_tap_gateway_payment": "Setup Tap Gateway Payment",
"configure_auto_billing_description": "Set up automatic billing with Tap Gateway for seamless recurring payments",
"tap_payment_processing": "Processing your payment through Tap Gateway...",
"payment_successful": "Payment completed successfully",
"payment_failed": "Payment failed. Please try again or contact support",
"redirecting_to_payment": "Redirecting you to Tap Gateway...",
"secure_payment_powered_by": "Secure payment powered by Tap Gateway",
"return_to_billing": "Return to Billing",
"payment_cancelled": "Payment was cancelled",
"invalid_payment_details": "Invalid payment details provided",
"payment_method_setup_complete": "Payment method has been set up successfully",
"auto_billing_enabled": "Automatic billing is now enabled",
"payment_amount": "Payment Amount",
"invoice_number": "Invoice Number",
"pay_now": "Pay Now",
"processing_payment": "Processing Payment...",
"setup_payment_method": "Setup Payment Method",
"payment_method_name": "Payment Method Name",
"my_tap_gateway_card": "My Tap Gateway Card",
"cancel": "Cancel",
"checkout": "Checkout",
"payment_details": "Payment Details",
"billing_address": "Billing Address",
"card_details": "Card Details",
"expiry_date": "Expiry Date",
"cvv": "CVV",
"card_holder_name": "Cardholder Name"
}
Translation Categories
The tenant translations are organized into these categories:
- Payment Setup: Messages for configuring auto-billing
- Payment Process: Status messages during payment flow
- Success/Error Messages: Feedback for completed transactions
- Form Labels: User interface text for payment forms
- Navigation: Text for buttons and links
Step 3: Create the Payment Gateway Service
Interface Implementation Required
Your payment gateway service must implement the PaymentGatewayInterface
to work with our billing system. This ensures consistency across all payment methods.
Now let's build the main service class that handles payment processing.
Create the Service Class
php artisan module:make-class Services/TapPaymentGateway TapGateway
<?php
namespace Modules\TapGateway\Services;
use App\Contracts\PaymentGatewayInterface;
use App\Models\Invoice\Invoice;
use App\Models\Plan;
use App\Models\Tenant;
use App\Services\Billing\TransactionResult;
class TapPaymentGateway implements PaymentGatewayInterface
{
protected string $apiKey;
protected string $secretKey;
protected bool $sandboxMode;
protected string $currency;
protected string $description;
public function __construct(
string $apiKey = '',
string $secretKey = '',
bool $sandboxMode = true,
string $currency = 'USD',
string $description = 'Payment via Tap Gateway'
) {
$this->apiKey = $apiKey;
$this->secretKey = $secretKey;
$this->sandboxMode = $sandboxMode;
$this->currency = $currency;
$this->description = $description;
}
/**
* Get the payment gateway name
*/
public function getName(): string
{
return 'Tap Gateway';
}
public function getType(): string
{
return 'tap_gateway';
}
public function getDescription(): string
{
return t('secure_payment_powered_by') . ' Tap Gateway';
}
/**
* Check if the payment gateway is properly configured and active
*/
public function isActive(): bool
{
// Gateway is active only if we have both API keys
return !empty($this->apiKey) && !empty($this->secretKey);
}
/**
* Get the checkout URL for an invoice
*/
public function getCheckoutUrl(Invoice $invoice): string
{
return tenant_route('tenant.payment.tap-gateway.checkout', ['invoice' => $invoice->id]);
}
/**
* Process a payment for an invoice
*/
public function processPayment(Invoice $invoice, array $paymentData = []): TransactionResult
{
try {
whatsapp_log('Starting Tap Gateway payment', 'info', [
'invoice_id' => $invoice->id,
'amount' => $invoice->total,
'currency' => $this->currency
]);
// This is where you'd integrate with Tap Gateway's API
// For now, we'll return a pending result that redirects to checkout
return new TransactionResult(
success: false, // Not completed yet, needs redirect
transactionId: null,
message: t('redirecting_to_payment'),
redirectUrl: $this->getCheckoutUrl($invoice)
);
} catch (\Exception $e) {
whatsapp_log('Tap Gateway payment failed', 'error', [
'invoice_id' => $invoice->id,
'error' => $e->getMessage()
], $e);
return new TransactionResult(
success: false,
transactionId: null,
message: t('payment_failed')
);
}
}
/**
* Handle webhook notifications from Tap Gateway
*/
public function handleWebhook(array $payload): bool
{
whatsapp_log('Tap Gateway webhook received', 'info', [
'payload_keys' => array_keys($payload)
]);
return true;
}
}
Step 4: Register Your Gateway with the Billing System
Gateway Registration
The billing manager needs to know about your payment gateway so it can be used for actual payments. This registration happens through events.
Now we need to tell the billing manager about your payment gateway so it can be used for actual payments.
Create Gateway Registration Listener
php artisan module:make-listener RegisterTapGateway TapGateway
<?php
namespace Modules\TapGateway\Listeners;
use App\Events\PaymentGatewayRegistration;
use App\Settings\PaymentSettings;
use Modules\TapGateway\Services\TapPaymentGateway;
use Nwidart\Modules\Facades\Module;
class RegisterTapGateway
{
public function handle(PaymentGatewayRegistration $event): void
{
// Only register if our module is active
if (!$this->isModuleActive()) {
return;
}
// Get payment settings from admin configuration
$settings = app(PaymentSettings::class);
// Only register gateway if it's enabled and properly configured
if ($settings->tap_gateway_enabled &&
!empty($settings->tap_gateway_api_key) &&
!empty($settings->tap_gateway_secret_key)) {
whatsapp_log('Registering Tap Gateway with billing manager', 'info');
// Register our gateway with the billing manager
$event->billingManager->register('tap_gateway', function () use ($settings) {
return new TapPaymentGateway(
$settings->tap_gateway_api_key,
$settings->tap_gateway_secret_key,
$settings->tap_gateway_sandbox_mode,
$settings->tap_gateway_currency ?? 'USD',
$settings->tap_gateway_description ?? 'Payment via Tap Gateway'
);
});
}
}
private function isModuleActive(): bool
{
return Module::find('TapGateway')?->isEnabled() ?? false;
}
}
Register the Gateway Service in Container
Service Container Binding
Laravel's service container allows us to bind our payment gateway service so it can be dependency-injected anywhere in the application.
Add this to the register()
method in TapGatewayServiceProvider.php
:
public function register()
{
// Bind payment gateway service to the container
$this->app->bind(TapPaymentGateway::class, function ($app) {
$settings = $app->make(PaymentSettings::class);
return new TapPaymentGateway(
$settings->tap_gateway_api_key ?? '',
$settings->tap_gateway_secret_key ?? '',
$settings->tap_gateway_sandbox_mode ?? true,
$settings->tap_gateway_currency ?? 'USD',
$settings->tap_gateway_description ?? 'Payment via Tap Gateway'
);
});
}
Step 5: Build the Tenant Payment Controllers
::: caution Security Considerations Payment controllers handle sensitive financial data. Always validate input, verify authenticity of callbacks, and log all payment-related activities. :::
Now let's create the controllers that handle the actual payment flow for tenants.
Create the Payment Controller
php artisan module:make-controller PaymentGateways/TapGatewayController TapGateway
<?php
namespace Modules\TapGateway\Http\Controllers\PaymentGateways;
use App\Http\Controllers\Controller;
use App\Models\Invoice\Invoice;
use App\Models\Transaction;
use App\Services\Billing\TransactionResult;
use Illuminate\Http\Request;
use Modules\TapGateway\Services\TapPaymentGateway;
class TapGatewayController extends Controller
{
protected TapPaymentGateway $paymentGateway;
public function __construct(TapPaymentGateway $paymentGateway)
{
$this->paymentGateway = $paymentGateway;
}
/**
* Show the checkout page for an invoice
*/
public function checkout(Request $request, string $subdomain, $invoiceId)
{
// Find the invoice for the current tenant
$invoice = Invoice::where('id', $invoiceId)
->where('tenant_id', tenant_id())
->firstOrFail();
// Check if payment gateway is active
if (!$this->paymentGateway->isActive()) {
return redirect()->back()
->with('error', t('payment_gateway_not_available'));
}
whatsapp_log('Tap Gateway checkout initiated', 'info', [
'invoice_id' => $invoice->id,
'tenant_id' => tenant_id(),
'amount' => $invoice->total
]);
return view('tapgateway::payment.checkout', [
'invoice' => $invoice,
'gateway' => $this->paymentGateway,
]);
}
/**
* Handle payment processing
*/
public function process(Request $request, string $subdomain, $invoiceId)
{
$invoice = Invoice::where('id', $invoiceId)
->where('tenant_id', tenant_id())
->firstOrFail();
try {
// Validate payment data
$request->validate([
'payment_token' => 'required|string',
'payment_method_id' => 'nullable|string',
]);
whatsapp_log('Processing Tap Gateway payment', 'info', [
'invoice_id' => $invoice->id,
'payment_token' => substr($request->payment_token, 0, 10) . '...'
]);
// Process payment through your gateway
$result = $this->paymentGateway->processPayment($invoice, [
'payment_token' => $request->payment_token,
'payment_method_id' => $request->payment_method_id,
]);
if ($result->success) {
return redirect()
->to(tenant_route('tenant.invoices.show', ['id' => $invoiceId]))
->with('success', t('payment_successful'));
} else {
return redirect()->back()
->with('error', $result->message ?? t('payment_failed'))
->withInput();
}
} catch (\Exception $e) {
whatsapp_log('Tap Gateway payment error', 'error', [
'invoice_id' => $invoice->id,
'error' => $e->getMessage()
], $e);
return redirect()->back()
->with('error', t('payment_failed'))
->withInput();
}
}
/**
* Handle payment callback from Tap Gateway
*/
public function callback(Request $request, string $subdomain, $invoiceId)
{
$invoice = Invoice::findOrFail($invoiceId);
whatsapp_log('Tap Gateway payment callback received', 'info', [
'invoice_id' => $invoiceId,
'status' => $request->get('status'),
'transaction_id' => $request->get('transaction_id')
]);
// Verify the callback is legitimate
if (!$this->verifyCallback($request)) {
whatsapp_log('Invalid Tap Gateway callback', 'warning', [
'invoice_id' => $invoiceId
]);
return redirect()
->to(tenant_route('tenant.invoices.show', ['id' => $invoiceId]))
->with('error', t('invalid_payment_details'));
}
$status = $request->get('status');
if ($status === 'success') {
// Payment successful - update invoice status
return redirect()
->to(tenant_route('tenant.invoices.show', ['id' => $invoiceId]))
->with('success', t('payment_successful'));
} else {
return redirect()
->to(tenant_route('tenant.invoices.show', ['id' => $invoiceId]))
->with('error', t('payment_failed'));
}
}
/**
* Setup auto-billing payment method
*/
public function setup(Request $request)
{
$returnUrl = $request->input('return_url', tenant_route('tenant.billing'));
if (!$this->paymentGateway->isActive()) {
return redirect($returnUrl)
->with('error', t('payment_gateway_not_available'));
}
return view('tapgateway::payment.setup', [
'returnUrl' => $returnUrl,
'gateway' => $this->paymentGateway,
]);
}
/**
* Handle webhook from Tap Gateway
*/
public function webhook(Request $request)
{
try {
$payload = $request->all();
whatsapp_log('Tap Gateway webhook received', 'info', [
'event_type' => $payload['event'] ?? 'unknown',
'object_id' => $payload['id'] ?? null
]);
// Verify webhook signature
if (!$this->verifyWebhookSignature($request)) {
whatsapp_log('Invalid Tap Gateway webhook signature', 'warning');
return response('Unauthorized', 401);
}
// Process the webhook
$result = $this->paymentGateway->handleWebhook($payload);
return response('OK', $result ? 200 : 400);
} catch (\Exception $e) {
whatsapp_log('Tap Gateway webhook error', 'error', [
'error' => $e->getMessage()
], $e);
return response('Error', 500);
}
}
private function verifyCallback(Request $request): bool
{
// Implement callback verification logic
return true; // Simplified for this example
}
private function verifyWebhookSignature(Request $request): bool
{
// Implement webhook signature verification
return true; // Simplified for this example
}
}
Step 6: Create Tenant Payment Views
User Experience Focus
The checkout and setup pages should provide a smooth, professional experience that builds trust with your customers during the payment process.
Now let's create the user interface that tenants will see when paying with your gateway.
Create the Checkout View
Create the file: Modules/TapGateway/resources/views/payment/checkout.blade.php
<x-app-layout>
<x-slot:title>{{ t('checkout') }} - {{ $invoice->invoice_number }}</x-slot:title>
<div class="max-w-2xl mx-auto sm:px-6 lg:px-8">
<!-- Payment Header -->
<div class="bg-white dark:bg-gray-800 shadow rounded-lg mb-6">
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
{{ t('pay_with_tap_gateway') }}
</h1>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ t('secure_payment_powered_by') }}
</p>
</div>
<div class="text-right">
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ t('invoice_number') }}
</div>
<div class="font-medium text-gray-900 dark:text-gray-100">
{{ $invoice->invoice_number }}
</div>
</div>
</div>
</div>
</div>
<!-- Payment Details -->
<div class="bg-white dark:bg-gray-800 shadow rounded-lg mb-6">
<div class="p-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
{{ t('payment_details') }}
</h2>
<div class="space-y-4">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('payment_amount') }}</span>
<span class="font-bold text-xl text-gray-900 dark:text-gray-100">
${{ number_format($invoice->total, 2) }}
</span>
</div>
</div>
</div>
</div>
<!-- Payment Form -->
<div class="bg-white dark:bg-gray-800 shadow rounded-lg">
<div class="p-6">
<form id="tap-payment-form" method="POST" action="{{ tenant_route('tenant.payment.tap-gateway.process', ['invoice' => $invoice->id]) }}"> // [!code focus]
@csrf
<!-- Card Details Section -->
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
{{ t('card_details') }}
</h3>
<div id="tap-card-element" class="p-4 border border-gray-300 dark:border-gray-600 rounded-lg">
<!-- Tap Gateway card element will be mounted here -->
<div class="animate-pulse">
<div class="h-10 bg-gray-200 dark:bg-gray-700 rounded mb-3"></div>
<div class="grid grid-cols-2 gap-3">
<div class="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
<div class="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
</div>
</div>
<div id="card-errors" class="mt-2 text-sm text-red-600" role="alert"></div>
</div>
<!-- Hidden fields for payment processing -->
<input type="hidden" name="payment_token" id="payment_token">
<input type="hidden" name="payment_method_id" id="payment_method_id">
<!-- Action Buttons -->
<div class="flex justify-between items-center pt-6 border-t border-gray-200 dark:border-gray-600">
<a href="{{ tenant_route('tenant.invoices.show', ['id' => $invoice->id]) }}"
class="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
{{ t('return_to_invoice') }}
</a>
<button type="submit" id="pay-button"
class="px-8 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed">
<span id="button-text">{{ t('pay_now') }} ${{ number_format($invoice->total, 2) }}</span>
<span id="loading-text" class="hidden">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ t('processing_payment') }}
</span>
</button>
</div>
</form>
</div>
</div>
<!-- Security Notice -->
<div class="mt-6 text-center">
<div class="flex items-center justify-center text-sm text-gray-500 dark:text-gray-400">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
{{ t('secure_payment_notice') }}
</div>