Files
website/website/routes/admin.php
Claude Dev 45d25d61ba Idempotent provisioning, service soft-delete, Plans page redesign, doc updates
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>
2026-02-10 06:30:57 -05:00

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');