99 lines
4.1 KiB
PHP
99 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace Modules\Licensing\Http\Controllers\Admin;
|
|
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Routing\Controller;
|
|
use Modules\Licensing\Models\License;
|
|
use Modules\Licensing\Models\LicenseProduct;
|
|
use Modules\Licensing\Models\LicenseWebhookEvent;
|
|
|
|
final class LicensingDashboardController extends Controller
|
|
{
|
|
public function index(): View
|
|
{
|
|
return view('licensing::admin.index', [
|
|
'licenseCount' => License::query()->count(),
|
|
'activeCount' => License::query()->whereNull('revoked_at')->where(function ($q): void {
|
|
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
|
|
})->count(),
|
|
'productCount' => LicenseProduct::query()->count(),
|
|
'recentLicenses' => License::query()->with('product')->latest()->limit(10)->get(),
|
|
]);
|
|
}
|
|
|
|
public function licenses(): View
|
|
{
|
|
return view('licensing::admin.licenses', [
|
|
'licenses' => License::query()->with('product')->latest()->paginate(25),
|
|
]);
|
|
}
|
|
|
|
public function show(License $license): View
|
|
{
|
|
return view('licensing::admin.show', [
|
|
'license' => $license->load(['product', 'activations']),
|
|
]);
|
|
}
|
|
|
|
/** Read-only snapshot of existing licensing subscription metadata. */
|
|
public function subscriptions(): View
|
|
{
|
|
$subscriptions = License::query()
|
|
->whereNotNull('external_subscription_id')
|
|
->with('product')
|
|
->orderByDesc('created_at')
|
|
->paginate(25);
|
|
|
|
$total = License::query()->whereNotNull('external_subscription_id')->count();
|
|
$active = License::query()->whereNotNull('external_subscription_id')
|
|
->whereIn('subscription_status', ['active', 'trialing'])->count();
|
|
$cancelScheduled = License::query()->whereNotNull('external_subscription_id')
|
|
->whereNotNull('cancel_scheduled_at')->count();
|
|
|
|
return view('licensing::admin.subscriptions', compact('subscriptions', 'total', 'active', 'cancelScheduled'));
|
|
}
|
|
|
|
/** Local-only billing integrity audit. Does not contact Paddle or mutate records. */
|
|
public function billingAudit(): View
|
|
{
|
|
$linked = License::query()->whereNotNull('external_subscription_id');
|
|
|
|
$duplicateSubscriptions = License::query()
|
|
->select('payment_provider', 'external_subscription_id')
|
|
->selectRaw('COUNT(*) AS license_count')
|
|
->whereNotNull('external_subscription_id')
|
|
->groupBy('payment_provider', 'external_subscription_id')
|
|
->havingRaw('COUNT(*) > 1')
|
|
->orderByDesc('license_count')
|
|
->limit(25)
|
|
->get();
|
|
|
|
$duplicateEvents = LicenseWebhookEvent::query()
|
|
->select('provider', 'external_event_id')
|
|
->selectRaw('COUNT(*) AS event_count')
|
|
->whereNotNull('external_event_id')
|
|
->groupBy('provider', 'external_event_id')
|
|
->havingRaw('COUNT(*) > 1')
|
|
->orderByDesc('event_count')
|
|
->limit(25)
|
|
->get();
|
|
|
|
return view('licensing::admin.billing-audit', [
|
|
'linkedCount' => (clone $linked)->count(),
|
|
'missingProvider' => (clone $linked)->whereNull('payment_provider')->count(),
|
|
'missingCustomer' => (clone $linked)->whereNull('external_customer_id')->count(),
|
|
'missingStatus' => (clone $linked)->whereNull('subscription_status')->count(),
|
|
'eventCount' => LicenseWebhookEvent::query()->count(),
|
|
'unprocessedEvents' => LicenseWebhookEvent::query()->whereNull('processed_at')->count(),
|
|
'failedEvents' => LicenseWebhookEvent::query()->whereNotNull('processing_error')->count(),
|
|
'duplicateSubscriptions' => $duplicateSubscriptions,
|
|
'duplicateEvents' => $duplicateEvents,
|
|
'recentFailures' => LicenseWebhookEvent::query()
|
|
->whereNotNull('processing_error')->latest()->limit(15)
|
|
->get(['id', 'provider', 'external_event_id', 'event_type', 'processing_error', 'created_at']),
|
|
]);
|
|
}
|
|
|
|
}
|