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>
122 lines
4.0 KiB
PHP
122 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Events\ProvisioningFailed;
|
|
use App\Models\ProvisioningLog;
|
|
use App\Models\Service;
|
|
use App\Services\Provisioning\ProvisioningFactory;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class RetryProvisioningCommand extends Command
|
|
{
|
|
protected $signature = 'provisioning:retry
|
|
{--max-attempts=3 : Maximum number of retry attempts per service}';
|
|
|
|
protected $description = 'Retry failed provisioning attempts';
|
|
|
|
public function handle(ProvisioningFactory $provisioningFactory): int
|
|
{
|
|
$maxAttempts = (int) $this->option('max-attempts');
|
|
|
|
$failedServices = Service::query()
|
|
->where('status', 'failed')
|
|
->where('created_at', '>=', now()->subDays(7))
|
|
->with(['subscription', 'plan'])
|
|
->get();
|
|
|
|
if ($failedServices->isEmpty()) {
|
|
$this->info('No failed services found to retry.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$this->info("Found {$failedServices->count()} failed service(s) to evaluate.");
|
|
|
|
$retried = 0;
|
|
$skipped = 0;
|
|
$succeeded = 0;
|
|
$failed = 0;
|
|
|
|
foreach ($failedServices as $service) {
|
|
$failedAttempts = ProvisioningLog::query()
|
|
->where('service_id', $service->id)
|
|
->where('action', 'provision')
|
|
->where('status', 'failed')
|
|
->count();
|
|
|
|
if ($failedAttempts >= $maxAttempts) {
|
|
$this->warn("Service #{$service->id}: skipped (already at {$failedAttempts}/{$maxAttempts} attempts).");
|
|
|
|
if ($failedAttempts === $maxAttempts) {
|
|
ProvisioningFailed::dispatch(
|
|
$service,
|
|
'Maximum retry attempts reached',
|
|
$failedAttempts,
|
|
true,
|
|
);
|
|
}
|
|
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (! $service->subscription) {
|
|
$this->warn("Service #{$service->id}: skipped (no associated subscription).");
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$attemptNumber = $failedAttempts + 1;
|
|
$this->info("Service #{$service->id}: retrying provisioning (attempt {$attemptNumber}/{$maxAttempts})...");
|
|
|
|
try {
|
|
$provisioningService = $provisioningFactory->make($service->service_type);
|
|
|
|
// provision() is idempotent — it reuses the existing Service record
|
|
$retryService = $provisioningService->provision($service->subscription);
|
|
|
|
$this->info("Service #{$retryService->id}: provisioned successfully.");
|
|
Log::info("Provisioning retry succeeded for service #{$retryService->id}", [
|
|
'service_id' => $retryService->id,
|
|
'service_type' => $service->service_type,
|
|
'attempt' => $attemptNumber,
|
|
]);
|
|
|
|
$retried++;
|
|
$succeeded++;
|
|
} catch (\Throwable $e) {
|
|
$this->error("Service #{$service->id}: provisioning failed — {$e->getMessage()}");
|
|
Log::error("Provisioning retry failed for service #{$service->id}", [
|
|
'service_id' => $service->id,
|
|
'service_type' => $service->service_type,
|
|
'attempt' => $attemptNumber,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
$isMaxReached = $attemptNumber >= $maxAttempts;
|
|
|
|
ProvisioningFailed::dispatch(
|
|
$service->fresh() ?? $service,
|
|
$e->getMessage(),
|
|
$attemptNumber,
|
|
$isMaxReached,
|
|
);
|
|
|
|
$retried++;
|
|
$failed++;
|
|
}
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info("Retry summary: {$retried} retried, {$succeeded} succeeded, {$failed} failed, {$skipped} skipped.");
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|