Files
website/website/app/Console/Commands/RetryProvisioningCommand.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

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;
}
}