move into sonardesk
This commit is contained in:
@@ -6,6 +6,7 @@ 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
|
||||
{
|
||||
@@ -53,4 +54,45 @@ final class LicensingDashboardController extends Controller
|
||||
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']),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
@extends('licensing::admin.layout')
|
||||
@section('title', 'SonarDesk Billing Audit')
|
||||
@section('content')
|
||||
<h1>Billing Audit</h1>
|
||||
<p class="muted">Local database checks only. No Paddle API requests, billing changes, webhook replays or licence mutations are performed.</p>
|
||||
<div class="cards">
|
||||
<div class="card"><div class="muted">Subscription-linked licences</div><div class="metric">{{ number_format($linkedCount) }}</div></div>
|
||||
<div class="card"><div class="muted">Missing payment provider</div><div class="metric">{{ number_format($missingProvider) }}</div></div>
|
||||
<div class="card"><div class="muted">Missing customer ID</div><div class="metric">{{ number_format($missingCustomer) }}</div></div>
|
||||
<div class="card"><div class="muted">Missing subscription status</div><div class="metric">{{ number_format($missingStatus) }}</div></div>
|
||||
<div class="card"><div class="muted">Webhook events stored</div><div class="metric">{{ number_format($eventCount) }}</div></div>
|
||||
<div class="card"><div class="muted">Unprocessed events</div><div class="metric">{{ number_format($unprocessedEvents) }}</div></div>
|
||||
<div class="card"><div class="muted">Events with errors</div><div class="metric">{{ number_format($failedEvents) }}</div></div>
|
||||
</div>
|
||||
<h2>Duplicate subscription identifiers (first 25)</h2>
|
||||
<p class="muted">Multiple licences can legitimately share a subscription. Review these before deciding whether a record is incorrect.</p>
|
||||
<div class="table-wrap"><table><thead><tr><th>Provider</th><th>Subscription ID</th><th>Licences</th></tr></thead><tbody>
|
||||
@forelse ($duplicateSubscriptions as $row)
|
||||
<tr><td>{{ $row->payment_provider ?? '—' }}</td><td>{{ $row->external_subscription_id }}</td><td>{{ $row->license_count }}</td></tr>
|
||||
@empty<tr><td colspan="3">No repeated subscription identifiers.</td></tr>@endforelse
|
||||
</tbody></table></div>
|
||||
<h2>Duplicate webhook event identifiers (first 25)</h2>
|
||||
<div class="table-wrap"><table><thead><tr><th>Provider</th><th>Event ID</th><th>Rows</th></tr></thead><tbody>
|
||||
@forelse ($duplicateEvents as $row)
|
||||
<tr><td>{{ $row->provider }}</td><td>{{ $row->external_event_id }}</td><td>{{ $row->event_count }}</td></tr>
|
||||
@empty<tr><td colspan="3">No duplicate event identifiers.</td></tr>@endforelse
|
||||
</tbody></table></div>
|
||||
<h2>Recent processing errors</h2>
|
||||
<div class="table-wrap"><table><thead><tr><th>When</th><th>Provider</th><th>Event type</th><th>Event ID</th><th>Error</th></tr></thead><tbody>
|
||||
@forelse ($recentFailures as $event)
|
||||
<tr><td>{{ $event->created_at?->format('d M Y H:i') ?? '—' }}</td><td>{{ $event->provider }}</td><td>{{ $event->event_type }}</td><td>{{ $event->external_event_id }}</td><td>{{ \Illuminate\Support\Str::limit($event->processing_error, 240) }}</td></tr>
|
||||
@empty<tr><td colspan="5">No stored processing errors.</td></tr>@endforelse
|
||||
</tbody></table></div>
|
||||
@endsection
|
||||
@@ -10,6 +10,7 @@ Route::middleware(['web', 'auth', 'verified', 'can:manage-sonardesk-licensing'])
|
||||
->group(function (): void {
|
||||
Route::get('/', [LicensingDashboardController::class, 'index'])->name('index');
|
||||
Route::get('/licenses', [LicensingDashboardController::class, 'licenses'])->name('licenses');
|
||||
Route::get('/billing-audit', [LicensingDashboardController::class, 'billingAudit'])->name('billing-audit');
|
||||
Route::get('/subscriptions', [LicensingDashboardController::class, 'subscriptions'])->name('subscriptions');
|
||||
Route::get('/licenses/{license}', [LicensingDashboardController::class, 'show'])->name('licenses.show');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user