Part A: Fix duplicate Service creation on provisioning retry - All 4 provisioning services use Service::firstOrCreate() keyed on subscription_id+service_type to prevent duplicates on queue retries - HandleSubscriptionCreated sends notification before provisioning, no longer re-throws on failure - RetryProvisioningCommand simplified to reuse existing Service records Part B: Plans/Pricing page complete redesign - Service type tabs (VPS, Dedicated, Web Hosting, MySQL) - Billing cycle segmented toggle (monthly/quarterly/semi-annual/annual) - Feature icons per service type, Popular/Best Value badges - Stock indicators, effective monthly price calculations Part C: Admin service soft-delete/archive - Service model uses SoftDeletes trait - Admin can archive and restore services - Show archived toggle on services list - Migration adds deleted_at column Docs: Updated TASKS.md, CLAUDE.md, PROJECT_DEVELOPMENT.md, MEMORY.md - Phase 3 marked complete, test counts updated (252 passing) - SupportPal references replaced with standalone ticket system - Frontend design skill background rule added - Closed GitHub issues #3, #6, #7, #8, #9 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
5.0 KiB
PHP
85 lines
5.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Http\Controllers\Admin\AuditLogController;
|
|
use App\Http\Controllers\Admin\CouponController;
|
|
use App\Http\Controllers\Admin\CustomerController;
|
|
use App\Http\Controllers\Admin\DashboardController;
|
|
use App\Http\Controllers\Admin\ImpersonationController;
|
|
use App\Http\Controllers\Admin\InvoiceController;
|
|
use App\Http\Controllers\Admin\OrderController;
|
|
use App\Http\Controllers\Admin\PlanController;
|
|
use App\Http\Controllers\Admin\ServiceController;
|
|
use App\Http\Controllers\Admin\SettingsController;
|
|
use App\Http\Controllers\Admin\TicketController as AdminTicketController;
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
Route::get('/dashboard', [DashboardController::class, 'index'])->name('admin.dashboard');
|
|
|
|
Route::resource('customers', CustomerController::class)->only(['index', 'show', 'edit', 'update'])->parameters(['customers' => 'user']);
|
|
Route::post('customers/{user}/suspend', [CustomerController::class, 'suspend'])->name('customers.suspend');
|
|
Route::post('customers/{user}/unsuspend', [CustomerController::class, 'unsuspend'])->name('customers.unsuspend');
|
|
Route::delete('customers/{user}/purge', [CustomerController::class, 'purge'])->name('customers.purge');
|
|
Route::post('customers/{user}/reset-password', [CustomerController::class, 'resetPassword'])->name('customers.reset-password');
|
|
Route::post('customers/{user}/send-notification', [CustomerController::class, 'sendNotification'])->name('customers.send-notification');
|
|
Route::post('customers/{user}/place-order', [CustomerController::class, 'placeOrder'])->name('customers.place-order');
|
|
|
|
Route::resource('plans', PlanController::class)->names([
|
|
'index' => 'admin.plans.index',
|
|
'create' => 'admin.plans.create',
|
|
'store' => 'admin.plans.store',
|
|
'edit' => 'admin.plans.edit',
|
|
'update' => 'admin.plans.update',
|
|
'destroy' => 'admin.plans.destroy',
|
|
])->except(['show']);
|
|
|
|
Route::resource('services', ServiceController::class)->only(['index', 'show', 'update', 'destroy']);
|
|
Route::post('services/{service}/suspend', [ServiceController::class, 'suspend'])->name('services.suspend');
|
|
Route::post('services/{service}/unsuspend', [ServiceController::class, 'unsuspend'])->name('services.unsuspend');
|
|
Route::post('services/{service}/terminate', [ServiceController::class, 'terminate'])->name('services.terminate');
|
|
Route::post('services/{service}/provision', [ServiceController::class, 'provision'])->name('services.provision');
|
|
Route::post('services/{service}/restore', [ServiceController::class, 'restore'])->name('services.restore');
|
|
|
|
Route::resource('invoices', InvoiceController::class)->only(['index', 'create', 'store', 'show', 'edit', 'update']);
|
|
Route::get('invoices/{invoice}/download', [InvoiceController::class, 'download'])->name('invoices.download');
|
|
Route::post('invoices/{invoice}/void', [InvoiceController::class, 'void'])->name('invoices.void');
|
|
Route::post('invoices/{invoice}/resend', [InvoiceController::class, 'resend'])->name('invoices.resend');
|
|
|
|
Route::get('coupons/redemptions', [CouponController::class, 'redemptions'])->name('admin.coupons.redemptions');
|
|
Route::resource('coupons', CouponController::class)->names([
|
|
'index' => 'admin.coupons.index',
|
|
'create' => 'admin.coupons.create',
|
|
'store' => 'admin.coupons.store',
|
|
'show' => 'admin.coupons.show',
|
|
'edit' => 'admin.coupons.edit',
|
|
'update' => 'admin.coupons.update',
|
|
'destroy' => 'admin.coupons.destroy',
|
|
]);
|
|
|
|
Route::resource('orders', OrderController::class)->only(['index', 'show']);
|
|
Route::post('orders/{order}/process', [OrderController::class, 'process'])->name('orders.process');
|
|
Route::post('orders/{order}/complete', [OrderController::class, 'complete'])->name('orders.complete');
|
|
Route::post('orders/{order}/cancel', [OrderController::class, 'cancel'])->name('orders.cancel');
|
|
Route::put('orders/{order}/notes', [OrderController::class, 'updateNotes'])->name('orders.notes');
|
|
|
|
Route::get('audit-logs/export', [AuditLogController::class, 'export'])->name('audit-logs.export');
|
|
Route::get('audit-logs', [AuditLogController::class, 'index'])->name('audit-logs.index');
|
|
|
|
Route::get('settings', [SettingsController::class, 'index'])->name('admin.settings.index');
|
|
Route::put('settings', [SettingsController::class, 'update'])->name('admin.settings.update');
|
|
Route::post('settings/test-api', [SettingsController::class, 'testApiConnection'])->name('admin.settings.test-api');
|
|
Route::post('settings/test-discord', [SettingsController::class, 'testDiscordWebhook'])->name('admin.settings.test-discord');
|
|
|
|
// Support Tickets
|
|
Route::resource('tickets', AdminTicketController::class)->only(['index', 'show'])->names([
|
|
'index' => 'admin.tickets.index',
|
|
'show' => 'admin.tickets.show',
|
|
]);
|
|
Route::post('tickets/{ticket}/reply', [AdminTicketController::class, 'reply'])->name('admin.tickets.reply');
|
|
Route::put('tickets/{ticket}/status', [AdminTicketController::class, 'updateStatus'])->name('admin.tickets.status');
|
|
|
|
// Impersonation
|
|
Route::post('impersonate/{user}', [ImpersonationController::class, 'start'])->name('impersonate.start');
|
|
Route::post('impersonate/stop', [ImpersonationController::class, 'stop'])->name('impersonate.stop');
|