How to decouple invoices from subscriptions, resolve pricing discrepancies, and synchronize payment gateway webhook payloads with support administration dashboards.
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.
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.
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();
});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.
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',
]
);
}
}
}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.
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.