Architecting a Subscription Billing Engine: Syncing Payment Services with Admin Panels

How to decouple invoices from subscriptions, resolve pricing discrepancies, and synchronize payment gateway webhook payloads with support administration dashboards.

Akram Hafaiedh
Akram Hafaiedh
Full Stack Developer
2026-02-264 min read
// BACKEND & PIPELINE ENGINEERING
PRODUCTION VERIFIED
TECHNICAL CASE STUDY

Architecting a Subscription Billing Engine: Syncing Payment Services with Admin Panels

PHP 8.3QUEUESPARALLEL WORKERS

Architecting a Subscription Billing Engine

In subscription-based SaaS applications, billing complexity increases when you mix customer-facing invoices, dynamic subscription tiers, and internal admin panels.

A common issue in billing development is failing to decouple invoices from actual subscription states: users see their active plans represented as a simple list of invoices, while support representatives view mismatched pricing in the admin panel due to asynchronous payment gateway Webhook drops. Let's look at how to build a unified syncing architecture.


The Architecture: Decoupling Invoices from Subscriptions

A robust billing database requires separate tables to map out the customer lifecycle:

1. Subscriptions Table: Manages status (active, canceled, past_due), plan tier, and renewal date.

2. Invoices Table: Records historical payment transactions, invoice PDFs, and payment status.

3. Support Admin Dashboard: Queries a unified billing engine API rather than calculating raw sums from partial database tables.


1. Designing the Relational Database

php
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Subscriptions Table Schema
Schema::create('subscriptions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained();
    $table->string('stripe_subscription_id')->unique();
    $table->string('stripe_price_id');
    $table->string('status'); class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// active, past_due, trialing, canceled
    $table->timestamp('trial_ends_at')->nullable();
    $table->timestamp('ends_at')->nullable(); class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Subscription end date
    $table->timestamps();
});

class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Invoices Table Schema
Schema::create('invoices', function (Blueprint $table) {
    $table->id();
    $table->foreignId('subscription_id')->constrained();
    $table->string('stripe_invoice_id')->unique();
    $table->integer('amount_paid'); class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Stored in cents (e.g., 2900 for $29.00)
    $table->string('currency');
    $table->string('status'); class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// paid, open, uncollectible
    $table->timestamps();
});

2. Syncing Gateway Webhooks Securely

To resolve pricing mismatches on admin dashboards, implement a robust Webhook receiver that updates the database state as soon as Stripe/PayPal registers a transaction.

php
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// app/Http/Controllers/BillingWebhookController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Subscription;
use App\Models\Invoice;

class BillingWebhookController extends Controller
{
    public function handle(Request $request)
    {
        $payload = $request->all();
        $eventType = $payload['type'];

        switch ($eventType) {
            case 'invoice.payment_succeeded':
                $this->processPaymentSucceeded($payload['data']['object']);
                break;
            case 'customer.subscription.updated':
                $this->processSubscriptionUpdated($payload['data']['object']);
                break;
        }

        return response('Webhook Handled', 200);
    }

    protected function processPaymentSucceeded(array $invoiceData)
    {
        $subscription = Subscription::where('stripe_subscription_id', $invoiceData['subscription'])->first();

        if ($subscription) {
            Invoice::updateOrCreate(
                ['stripe_invoice_id' => $invoiceData['id']],
                [
                    'subscription_id' => $subscription->id,
                    'amount_paid' => $invoiceData['amount_paid'], class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Stored in cents
                    'currency' => $invoiceData['currency'],
                    'status' => 'paid',
                ]
            );
        }
    }
}

3. Resolving Support Admin Panel Sum Discrepancies

When computing user subscription prices for support administrators, format monetary values in cents, and convert currency using localized standard helpers. This eliminates floating-point rounding errors.

php
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// app/Models/Invoice.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Invoice extends Model
{
    class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Accessor: Convert cents to dollars for display
    public function getFormattedAmountAttribute()
    {
        return '$' . number_format($this->amount_paid / 100, 2);
    }
}

By separating subscriptions from invoice histories and using atomic Webhook handlers, your application ensures a clean, reliable, and discrepancy-free billing pipeline.

Engineering Collaboration

Building a High-Scale SaaS or Complex System?

Let’s discuss your architecture, full-stack development, or system modernization needs.

Related Articles

// BACKEND & PIPELINE ENGINEERING
PRODUCTION VERIFIED
TECHNICAL CASE STUDY

Eliminating N+1 Queries & Indexing Multi-Tenant Tables: Optimizing Slow Database Queries under Heavy SaaS Load

PHP 8.3QUEUESPARALLEL WORKERS
Backend
2026-03-30•Backend

Eliminating N+1 Queries & Indexing Multi-Tenant Tables: Optimizing Slow Database Queries under Heavy SaaS Load

Read Article
// BACKEND & PIPELINE ENGINEERING
PRODUCTION VERIFIED
TECHNICAL CASE STUDY

Streaming Large Financial Exports: Preventing Memory Overflows in Laravel & Vue 3

PHP 8.3QUEUESPARALLEL WORKERS
Backend
2026-03-25•Backend

Streaming Large Financial Exports: Preventing Memory Overflows in Laravel & Vue 3

Read Article