
- Deleted obsolete CloudCheckSubscription, CloudCleanupSubscriptions, and CloudDeleteUser commands to streamline the codebase. - Introduced new CloudDeleteUser and CloudFixSubscription commands with improved functionality for user deletion and subscription management. - Enhanced subscription handling with options for fixing canceled subscriptions and verifying active subscriptions against Stripe, improving overall command usability and control.
723 lines
28 KiB
PHP
723 lines
28 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands\Cloud;
|
|
|
|
use App\Actions\Stripe\CancelSubscription;
|
|
use App\Actions\User\DeleteUserResources;
|
|
use App\Actions\User\DeleteUserServers;
|
|
use App\Actions\User\DeleteUserTeams;
|
|
use App\Models\User;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class CloudDeleteUser extends Command
|
|
{
|
|
protected $signature = 'cloud:delete-user {email}
|
|
{--dry-run : Preview what will be deleted without actually deleting}
|
|
{--skip-stripe : Skip Stripe subscription cancellation}
|
|
{--skip-resources : Skip resource deletion}';
|
|
|
|
protected $description = 'Delete a user from the cloud instance with phase-by-phase confirmation';
|
|
|
|
private bool $isDryRun = false;
|
|
|
|
private bool $skipStripe = false;
|
|
|
|
private bool $skipResources = false;
|
|
|
|
private User $user;
|
|
|
|
public function handle()
|
|
{
|
|
if (! isCloud()) {
|
|
$this->error('This command is only available on cloud instances.');
|
|
|
|
return 1;
|
|
}
|
|
|
|
$email = $this->argument('email');
|
|
$this->isDryRun = $this->option('dry-run');
|
|
$this->skipStripe = $this->option('skip-stripe');
|
|
$this->skipResources = $this->option('skip-resources');
|
|
|
|
if ($this->isDryRun) {
|
|
$this->info('🔍 DRY RUN MODE - No data will be deleted');
|
|
$this->newLine();
|
|
}
|
|
|
|
try {
|
|
$this->user = User::whereEmail($email)->firstOrFail();
|
|
} catch (\Exception $e) {
|
|
$this->error("User with email '{$email}' not found.");
|
|
|
|
return 1;
|
|
}
|
|
|
|
$this->logAction("Starting user deletion process for: {$email}");
|
|
|
|
// Phase 1: Show User Overview (outside transaction)
|
|
if (! $this->showUserOverview()) {
|
|
$this->info('User deletion cancelled.');
|
|
|
|
return 0;
|
|
}
|
|
|
|
// If not dry run, wrap everything in a transaction
|
|
if (! $this->isDryRun) {
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
// Phase 2: Delete Resources
|
|
if (! $this->skipResources) {
|
|
if (! $this->deleteResources()) {
|
|
DB::rollBack();
|
|
$this->error('User deletion failed at resource deletion phase. All changes rolled back.');
|
|
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Phase 3: Delete Servers
|
|
if (! $this->deleteServers()) {
|
|
DB::rollBack();
|
|
$this->error('User deletion failed at server deletion phase. All changes rolled back.');
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Phase 4: Handle Teams
|
|
if (! $this->handleTeams()) {
|
|
DB::rollBack();
|
|
$this->error('User deletion failed at team handling phase. All changes rolled back.');
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Phase 5: Cancel Stripe Subscriptions
|
|
if (! $this->skipStripe && isCloud()) {
|
|
if (! $this->cancelStripeSubscriptions()) {
|
|
DB::rollBack();
|
|
$this->error('User deletion failed at Stripe cancellation phase. All changes rolled back.');
|
|
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Phase 6: Delete User Profile
|
|
if (! $this->deleteUserProfile()) {
|
|
DB::rollBack();
|
|
$this->error('User deletion failed at final phase. All changes rolled back.');
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Commit the transaction
|
|
DB::commit();
|
|
|
|
$this->newLine();
|
|
$this->info('✅ User deletion completed successfully!');
|
|
$this->logAction("User deletion completed for: {$email}");
|
|
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
$this->error('An error occurred during user deletion: '.$e->getMessage());
|
|
$this->logAction("User deletion failed for {$email}: ".$e->getMessage());
|
|
|
|
return 1;
|
|
}
|
|
} else {
|
|
// Dry run mode - just run through the phases without transaction
|
|
// Phase 2: Delete Resources
|
|
if (! $this->skipResources) {
|
|
if (! $this->deleteResources()) {
|
|
$this->info('User deletion would be cancelled at resource deletion phase.');
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Phase 3: Delete Servers
|
|
if (! $this->deleteServers()) {
|
|
$this->info('User deletion would be cancelled at server deletion phase.');
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Phase 4: Handle Teams
|
|
if (! $this->handleTeams()) {
|
|
$this->info('User deletion would be cancelled at team handling phase.');
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Phase 5: Cancel Stripe Subscriptions
|
|
if (! $this->skipStripe && isCloud()) {
|
|
if (! $this->cancelStripeSubscriptions()) {
|
|
$this->info('User deletion would be cancelled at Stripe cancellation phase.');
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Phase 6: Delete User Profile
|
|
if (! $this->deleteUserProfile()) {
|
|
$this->info('User deletion would be cancelled at final phase.');
|
|
|
|
return 0;
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info('✅ DRY RUN completed successfully! No data was deleted.');
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private function showUserOverview(): bool
|
|
{
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->info('PHASE 1: USER OVERVIEW');
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->newLine();
|
|
|
|
$teams = $this->user->teams;
|
|
$ownedTeams = $teams->filter(fn ($team) => $team->pivot->role === 'owner');
|
|
$memberTeams = $teams->filter(fn ($team) => $team->pivot->role !== 'owner');
|
|
|
|
// Collect all servers from all teams
|
|
$allServers = collect();
|
|
$allApplications = collect();
|
|
$allDatabases = collect();
|
|
$allServices = collect();
|
|
$activeSubscriptions = collect();
|
|
|
|
foreach ($teams as $team) {
|
|
$servers = $team->servers;
|
|
$allServers = $allServers->merge($servers);
|
|
|
|
foreach ($servers as $server) {
|
|
$resources = $server->definedResources();
|
|
foreach ($resources as $resource) {
|
|
if ($resource instanceof \App\Models\Application) {
|
|
$allApplications->push($resource);
|
|
} elseif ($resource instanceof \App\Models\Service) {
|
|
$allServices->push($resource);
|
|
} else {
|
|
$allDatabases->push($resource);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($team->subscription && $team->subscription->stripe_subscription_id) {
|
|
$activeSubscriptions->push($team->subscription);
|
|
}
|
|
}
|
|
|
|
$this->table(
|
|
['Property', 'Value'],
|
|
[
|
|
['User', $this->user->email],
|
|
['User ID', $this->user->id],
|
|
['Created', $this->user->created_at->format('Y-m-d H:i:s')],
|
|
['Last Login', $this->user->updated_at->format('Y-m-d H:i:s')],
|
|
['Teams (Total)', $teams->count()],
|
|
['Teams (Owner)', $ownedTeams->count()],
|
|
['Teams (Member)', $memberTeams->count()],
|
|
['Servers', $allServers->unique('id')->count()],
|
|
['Applications', $allApplications->count()],
|
|
['Databases', $allDatabases->count()],
|
|
['Services', $allServices->count()],
|
|
['Active Stripe Subscriptions', $activeSubscriptions->count()],
|
|
]
|
|
);
|
|
|
|
$this->newLine();
|
|
|
|
$this->warn('⚠️ WARNING: This will permanently delete the user and all associated data!');
|
|
$this->newLine();
|
|
|
|
if (! $this->confirm('Do you want to continue with the deletion process?', false)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function deleteResources(): bool
|
|
{
|
|
$this->newLine();
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->info('PHASE 2: DELETE RESOURCES');
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->newLine();
|
|
|
|
$action = new DeleteUserResources($this->user, $this->isDryRun);
|
|
$resources = $action->getResourcesPreview();
|
|
|
|
if ($resources['applications']->isEmpty() &&
|
|
$resources['databases']->isEmpty() &&
|
|
$resources['services']->isEmpty()) {
|
|
$this->info('No resources to delete.');
|
|
|
|
return true;
|
|
}
|
|
|
|
$this->info('Resources to be deleted:');
|
|
$this->newLine();
|
|
|
|
if ($resources['applications']->isNotEmpty()) {
|
|
$this->warn("Applications to be deleted ({$resources['applications']->count()}):");
|
|
$this->table(
|
|
['Name', 'UUID', 'Server', 'Status'],
|
|
$resources['applications']->map(function ($app) {
|
|
return [
|
|
$app->name,
|
|
$app->uuid,
|
|
$app->destination->server->name,
|
|
$app->status ?? 'unknown',
|
|
];
|
|
})->toArray()
|
|
);
|
|
$this->newLine();
|
|
}
|
|
|
|
if ($resources['databases']->isNotEmpty()) {
|
|
$this->warn("Databases to be deleted ({$resources['databases']->count()}):");
|
|
$this->table(
|
|
['Name', 'Type', 'UUID', 'Server'],
|
|
$resources['databases']->map(function ($db) {
|
|
return [
|
|
$db->name,
|
|
class_basename($db),
|
|
$db->uuid,
|
|
$db->destination->server->name,
|
|
];
|
|
})->toArray()
|
|
);
|
|
$this->newLine();
|
|
}
|
|
|
|
if ($resources['services']->isNotEmpty()) {
|
|
$this->warn("Services to be deleted ({$resources['services']->count()}):");
|
|
$this->table(
|
|
['Name', 'UUID', 'Server'],
|
|
$resources['services']->map(function ($service) {
|
|
return [
|
|
$service->name,
|
|
$service->uuid,
|
|
$service->server->name,
|
|
];
|
|
})->toArray()
|
|
);
|
|
$this->newLine();
|
|
}
|
|
|
|
$this->error('⚠️ THIS ACTION CANNOT BE UNDONE!');
|
|
if (! $this->confirm('Are you sure you want to delete all these resources?', false)) {
|
|
return false;
|
|
}
|
|
|
|
if (! $this->isDryRun) {
|
|
$this->info('Deleting resources...');
|
|
$result = $action->execute();
|
|
$this->info("Deleted: {$result['applications']} applications, {$result['databases']} databases, {$result['services']} services");
|
|
$this->logAction("Deleted resources for user {$this->user->email}: {$result['applications']} apps, {$result['databases']} databases, {$result['services']} services");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function deleteServers(): bool
|
|
{
|
|
$this->newLine();
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->info('PHASE 3: DELETE SERVERS');
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->newLine();
|
|
|
|
$action = new DeleteUserServers($this->user, $this->isDryRun);
|
|
$servers = $action->getServersPreview();
|
|
|
|
if ($servers->isEmpty()) {
|
|
$this->info('No servers to delete.');
|
|
|
|
return true;
|
|
}
|
|
|
|
$this->warn("Servers to be deleted ({$servers->count()}):");
|
|
$this->table(
|
|
['ID', 'Name', 'IP', 'Description', 'Resources Count'],
|
|
$servers->map(function ($server) {
|
|
$resourceCount = $server->definedResources()->count();
|
|
|
|
return [
|
|
$server->id,
|
|
$server->name,
|
|
$server->ip,
|
|
$server->description ?? '-',
|
|
$resourceCount,
|
|
];
|
|
})->toArray()
|
|
);
|
|
$this->newLine();
|
|
|
|
$this->error('⚠️ WARNING: Deleting servers will remove all server configurations!');
|
|
if (! $this->confirm('Are you sure you want to delete all these servers?', false)) {
|
|
return false;
|
|
}
|
|
|
|
if (! $this->isDryRun) {
|
|
$this->info('Deleting servers...');
|
|
$result = $action->execute();
|
|
$this->info("Deleted {$result['servers']} servers");
|
|
$this->logAction("Deleted {$result['servers']} servers for user {$this->user->email}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function handleTeams(): bool
|
|
{
|
|
$this->newLine();
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->info('PHASE 4: HANDLE TEAMS');
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->newLine();
|
|
|
|
$action = new DeleteUserTeams($this->user, $this->isDryRun);
|
|
$preview = $action->getTeamsPreview();
|
|
|
|
// Check for edge cases first - EXIT IMMEDIATELY if found
|
|
if ($preview['edge_cases']->isNotEmpty()) {
|
|
$this->error('═══════════════════════════════════════');
|
|
$this->error('⚠️ EDGE CASES DETECTED - CANNOT PROCEED');
|
|
$this->error('═══════════════════════════════════════');
|
|
$this->newLine();
|
|
|
|
foreach ($preview['edge_cases'] as $edgeCase) {
|
|
$team = $edgeCase['team'];
|
|
$reason = $edgeCase['reason'];
|
|
$this->error("Team: {$team->name} (ID: {$team->id})");
|
|
$this->error("Issue: {$reason}");
|
|
|
|
// Show team members for context
|
|
$this->info('Current members:');
|
|
foreach ($team->members as $member) {
|
|
$role = $member->pivot->role;
|
|
$this->line(" - {$member->name} ({$member->email}) - Role: {$role}");
|
|
}
|
|
|
|
// Check for active resources
|
|
$resourceCount = 0;
|
|
foreach ($team->servers as $server) {
|
|
$resources = $server->definedResources();
|
|
$resourceCount += $resources->count();
|
|
}
|
|
|
|
if ($resourceCount > 0) {
|
|
$this->warn(" ⚠️ This team has {$resourceCount} active resources!");
|
|
}
|
|
|
|
// Show subscription details if relevant
|
|
if ($team->subscription && $team->subscription->stripe_subscription_id) {
|
|
$this->warn(' ⚠️ Active Stripe subscription details:');
|
|
$this->warn(" Subscription ID: {$team->subscription->stripe_subscription_id}");
|
|
$this->warn(" Customer ID: {$team->subscription->stripe_customer_id}");
|
|
|
|
// Show other owners who could potentially take over
|
|
$otherOwners = $team->members
|
|
->where('id', '!=', $this->user->id)
|
|
->filter(function ($member) {
|
|
return $member->pivot->role === 'owner';
|
|
});
|
|
|
|
if ($otherOwners->isNotEmpty()) {
|
|
$this->info(' Other owners who could take over billing:');
|
|
foreach ($otherOwners as $owner) {
|
|
$this->line(" - {$owner->name} ({$owner->email})");
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->newLine();
|
|
}
|
|
|
|
$this->error('Please resolve these issues manually before retrying:');
|
|
|
|
// Check if any edge case involves subscription payment issues
|
|
$hasSubscriptionIssue = $preview['edge_cases']->contains(function ($edgeCase) {
|
|
return str_contains($edgeCase['reason'], 'Stripe subscription');
|
|
});
|
|
|
|
if ($hasSubscriptionIssue) {
|
|
$this->info('For teams with subscription payment issues:');
|
|
$this->info('1. Cancel the subscription through Stripe dashboard, OR');
|
|
$this->info('2. Transfer the subscription to another owner\'s payment method, OR');
|
|
$this->info('3. Have the other owner create a new subscription after cancelling this one');
|
|
$this->newLine();
|
|
}
|
|
|
|
$hasNoOwnerReplacement = $preview['edge_cases']->contains(function ($edgeCase) {
|
|
return str_contains($edgeCase['reason'], 'No suitable owner replacement');
|
|
});
|
|
|
|
if ($hasNoOwnerReplacement) {
|
|
$this->info('For teams with no suitable owner replacement:');
|
|
$this->info('1. Assign an admin role to a trusted member, OR');
|
|
$this->info('2. Transfer team resources to another team, OR');
|
|
$this->info('3. Delete the team manually if no longer needed');
|
|
$this->newLine();
|
|
}
|
|
|
|
$this->error('USER DELETION ABORTED DUE TO EDGE CASES');
|
|
$this->logAction("User deletion aborted for {$this->user->email}: Edge cases in team handling");
|
|
|
|
// Exit immediately - don't proceed with deletion
|
|
if (! $this->isDryRun) {
|
|
DB::rollBack();
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
if ($preview['to_delete']->isEmpty() &&
|
|
$preview['to_transfer']->isEmpty() &&
|
|
$preview['to_leave']->isEmpty()) {
|
|
$this->info('No team changes needed.');
|
|
|
|
return true;
|
|
}
|
|
|
|
if ($preview['to_delete']->isNotEmpty()) {
|
|
$this->warn('Teams to be DELETED (user is the only member):');
|
|
$this->table(
|
|
['ID', 'Name', 'Resources', 'Subscription'],
|
|
$preview['to_delete']->map(function ($team) {
|
|
$resourceCount = 0;
|
|
foreach ($team->servers as $server) {
|
|
$resourceCount += $server->definedResources()->count();
|
|
}
|
|
$hasSubscription = $team->subscription && $team->subscription->stripe_subscription_id
|
|
? '⚠️ YES - '.$team->subscription->stripe_subscription_id
|
|
: 'No';
|
|
|
|
return [
|
|
$team->id,
|
|
$team->name,
|
|
$resourceCount,
|
|
$hasSubscription,
|
|
];
|
|
})->toArray()
|
|
);
|
|
$this->newLine();
|
|
}
|
|
|
|
if ($preview['to_transfer']->isNotEmpty()) {
|
|
$this->warn('Teams where ownership will be TRANSFERRED:');
|
|
$this->table(
|
|
['Team ID', 'Team Name', 'New Owner', 'New Owner Email'],
|
|
$preview['to_transfer']->map(function ($item) {
|
|
return [
|
|
$item['team']->id,
|
|
$item['team']->name,
|
|
$item['new_owner']->name,
|
|
$item['new_owner']->email,
|
|
];
|
|
})->toArray()
|
|
);
|
|
$this->newLine();
|
|
}
|
|
|
|
if ($preview['to_leave']->isNotEmpty()) {
|
|
$this->warn('Teams where user will be REMOVED (other owners/admins exist):');
|
|
$userId = $this->user->id;
|
|
$this->table(
|
|
['ID', 'Name', 'User Role', 'Other Members'],
|
|
$preview['to_leave']->map(function ($team) use ($userId) {
|
|
$userRole = $team->members->where('id', $userId)->first()->pivot->role;
|
|
$otherMembers = $team->members->count() - 1;
|
|
|
|
return [
|
|
$team->id,
|
|
$team->name,
|
|
$userRole,
|
|
$otherMembers,
|
|
];
|
|
})->toArray()
|
|
);
|
|
$this->newLine();
|
|
}
|
|
|
|
$this->error('⚠️ WARNING: Team changes affect access control and ownership!');
|
|
if (! $this->confirm('Are you sure you want to proceed with these team changes?', false)) {
|
|
return false;
|
|
}
|
|
|
|
if (! $this->isDryRun) {
|
|
$this->info('Processing team changes...');
|
|
$result = $action->execute();
|
|
$this->info("Teams deleted: {$result['deleted']}, ownership transferred: {$result['transferred']}, left: {$result['left']}");
|
|
$this->logAction("Team changes for user {$this->user->email}: deleted {$result['deleted']}, transferred {$result['transferred']}, left {$result['left']}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function cancelStripeSubscriptions(): bool
|
|
{
|
|
$this->newLine();
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->info('PHASE 5: CANCEL STRIPE SUBSCRIPTIONS');
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->newLine();
|
|
|
|
$action = new CancelSubscription($this->user, $this->isDryRun);
|
|
$subscriptions = $action->getSubscriptionsPreview();
|
|
|
|
if ($subscriptions->isEmpty()) {
|
|
$this->info('No Stripe subscriptions to cancel.');
|
|
|
|
return true;
|
|
}
|
|
|
|
$this->info('Stripe subscriptions to cancel:');
|
|
$this->newLine();
|
|
|
|
$totalMonthlyValue = 0;
|
|
foreach ($subscriptions as $subscription) {
|
|
$team = $subscription->team;
|
|
$planId = $subscription->stripe_plan_id;
|
|
|
|
// Try to get the price from config
|
|
$monthlyValue = $this->getSubscriptionMonthlyValue($planId);
|
|
$totalMonthlyValue += $monthlyValue;
|
|
|
|
$this->line(" - {$subscription->stripe_subscription_id} (Team: {$team->name})");
|
|
if ($monthlyValue > 0) {
|
|
$this->line(" Monthly value: \${$monthlyValue}");
|
|
}
|
|
if ($subscription->stripe_cancel_at_period_end) {
|
|
$this->line(' ⚠️ Already set to cancel at period end');
|
|
}
|
|
}
|
|
|
|
if ($totalMonthlyValue > 0) {
|
|
$this->newLine();
|
|
$this->warn("Total monthly value: \${$totalMonthlyValue}");
|
|
}
|
|
$this->newLine();
|
|
|
|
$this->error('⚠️ WARNING: Subscriptions will be cancelled IMMEDIATELY (not at period end)!');
|
|
if (! $this->confirm('Are you sure you want to cancel all these subscriptions immediately?', false)) {
|
|
return false;
|
|
}
|
|
|
|
if (! $this->isDryRun) {
|
|
$this->info('Cancelling subscriptions...');
|
|
$result = $action->execute();
|
|
$this->info("Cancelled {$result['cancelled']} subscriptions, {$result['failed']} failed");
|
|
if ($result['failed'] > 0 && ! empty($result['errors'])) {
|
|
$this->error('Failed subscriptions:');
|
|
foreach ($result['errors'] as $error) {
|
|
$this->error(" - {$error}");
|
|
}
|
|
}
|
|
$this->logAction("Cancelled {$result['cancelled']} Stripe subscriptions for user {$this->user->email}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function deleteUserProfile(): bool
|
|
{
|
|
$this->newLine();
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->info('PHASE 6: DELETE USER PROFILE');
|
|
$this->info('═══════════════════════════════════════');
|
|
$this->newLine();
|
|
|
|
$this->warn('⚠️ FINAL STEP - This action is IRREVERSIBLE!');
|
|
$this->newLine();
|
|
|
|
$this->info('User profile to be deleted:');
|
|
$this->table(
|
|
['Property', 'Value'],
|
|
[
|
|
['Email', $this->user->email],
|
|
['Name', $this->user->name],
|
|
['User ID', $this->user->id],
|
|
['Created', $this->user->created_at->format('Y-m-d H:i:s')],
|
|
['Email Verified', $this->user->email_verified_at ? 'Yes' : 'No'],
|
|
['2FA Enabled', $this->user->two_factor_confirmed_at ? 'Yes' : 'No'],
|
|
]
|
|
);
|
|
|
|
$this->newLine();
|
|
|
|
$this->warn("Type 'DELETE {$this->user->email}' to confirm final deletion:");
|
|
$confirmation = $this->ask('Confirmation');
|
|
|
|
if ($confirmation !== "DELETE {$this->user->email}") {
|
|
$this->error('Confirmation text does not match. Deletion cancelled.');
|
|
|
|
return false;
|
|
}
|
|
|
|
if (! $this->isDryRun) {
|
|
$this->info('Deleting user profile...');
|
|
|
|
try {
|
|
$this->user->delete();
|
|
$this->info('User profile deleted successfully.');
|
|
$this->logAction("User profile deleted: {$this->user->email}");
|
|
} catch (\Exception $e) {
|
|
$this->error('Failed to delete user profile: '.$e->getMessage());
|
|
$this->logAction("Failed to delete user profile {$this->user->email}: ".$e->getMessage());
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function getSubscriptionMonthlyValue(string $planId): int
|
|
{
|
|
// Map plan IDs to monthly values based on config
|
|
$subscriptionConfigs = config('subscription');
|
|
|
|
foreach ($subscriptionConfigs as $key => $value) {
|
|
if ($value === $planId && str_contains($key, 'stripe_price_id_')) {
|
|
// Extract price from key pattern: stripe_price_id_basic_monthly -> basic
|
|
$planType = str($key)->after('stripe_price_id_')->before('_')->toString();
|
|
|
|
// Map to known prices (you may need to adjust these based on your actual pricing)
|
|
return match ($planType) {
|
|
'basic' => 29,
|
|
'pro' => 49,
|
|
'ultimate' => 99,
|
|
default => 0
|
|
};
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private function logAction(string $message): void
|
|
{
|
|
$logMessage = "[CloudDeleteUser] {$message}";
|
|
|
|
if ($this->isDryRun) {
|
|
$logMessage = "[DRY RUN] {$logMessage}";
|
|
}
|
|
|
|
Log::channel('single')->info($logMessage);
|
|
|
|
// Also log to a dedicated user deletion log file
|
|
$logFile = storage_path('logs/user-deletions.log');
|
|
$timestamp = now()->format('Y-m-d H:i:s');
|
|
file_put_contents($logFile, "[{$timestamp}] {$logMessage}\n", FILE_APPEND | LOCK_EX);
|
|
}
|
|
}
|