203 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			203 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| namespace App\Actions\User;
 | |
| 
 | |
| use App\Models\Team;
 | |
| use App\Models\User;
 | |
| 
 | |
| class DeleteUserTeams
 | |
| {
 | |
|     private User $user;
 | |
| 
 | |
|     private bool $isDryRun;
 | |
| 
 | |
|     public function __construct(User $user, bool $isDryRun = false)
 | |
|     {
 | |
|         $this->user = $user;
 | |
|         $this->isDryRun = $isDryRun;
 | |
|     }
 | |
| 
 | |
|     public function getTeamsPreview(): array
 | |
|     {
 | |
|         $teamsToDelete = collect();
 | |
|         $teamsToTransfer = collect();
 | |
|         $teamsToLeave = collect();
 | |
|         $edgeCases = collect();
 | |
| 
 | |
|         $teams = $this->user->teams;
 | |
| 
 | |
|         foreach ($teams as $team) {
 | |
|             // Skip root team (ID 0)
 | |
|             if ($team->id === 0) {
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             $userRole = $team->pivot->role;
 | |
|             $memberCount = $team->members->count();
 | |
| 
 | |
|             if ($memberCount === 1) {
 | |
|                 // User is alone in the team - delete it
 | |
|                 $teamsToDelete->push($team);
 | |
|             } elseif ($userRole === 'owner') {
 | |
|                 // Check if there are other owners
 | |
|                 $otherOwners = $team->members
 | |
|                     ->where('id', '!=', $this->user->id)
 | |
|                     ->filter(function ($member) {
 | |
|                         return $member->pivot->role === 'owner';
 | |
|                     });
 | |
| 
 | |
|                 if ($otherOwners->isNotEmpty()) {
 | |
|                     // There are other owners, but check if this user is paying for the subscription
 | |
|                     if ($this->isUserPayingForTeamSubscription($team)) {
 | |
|                         // User is paying for the subscription - this is an edge case
 | |
|                         $edgeCases->push([
 | |
|                             'team' => $team,
 | |
|                             'reason' => 'User is paying for the team\'s Stripe subscription but there are other owners. The subscription needs to be cancelled or transferred to another owner\'s payment method.',
 | |
|                         ]);
 | |
|                     } else {
 | |
|                         // There are other owners and user is not paying, just remove this user
 | |
|                         $teamsToLeave->push($team);
 | |
|                     }
 | |
|                 } else {
 | |
|                     // User is the only owner, check for replacement
 | |
|                     $newOwner = $this->findNewOwner($team);
 | |
|                     if ($newOwner) {
 | |
|                         $teamsToTransfer->push([
 | |
|                             'team' => $team,
 | |
|                             'new_owner' => $newOwner,
 | |
|                         ]);
 | |
|                     } else {
 | |
|                         // No suitable replacement found - this is an edge case
 | |
|                         $edgeCases->push([
 | |
|                             'team' => $team,
 | |
|                             'reason' => 'No suitable owner replacement found. Team has only regular members without admin privileges.',
 | |
|                         ]);
 | |
|                     }
 | |
|                 }
 | |
|             } else {
 | |
|                 // User is just a member - remove them from the team
 | |
|                 $teamsToLeave->push($team);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return [
 | |
|             'to_delete' => $teamsToDelete,
 | |
|             'to_transfer' => $teamsToTransfer,
 | |
|             'to_leave' => $teamsToLeave,
 | |
|             'edge_cases' => $edgeCases,
 | |
|         ];
 | |
|     }
 | |
| 
 | |
|     public function execute(): array
 | |
|     {
 | |
|         if ($this->isDryRun) {
 | |
|             return [
 | |
|                 'deleted' => 0,
 | |
|                 'transferred' => 0,
 | |
|                 'left' => 0,
 | |
|             ];
 | |
|         }
 | |
| 
 | |
|         $counts = [
 | |
|             'deleted' => 0,
 | |
|             'transferred' => 0,
 | |
|             'left' => 0,
 | |
|         ];
 | |
| 
 | |
|         $preview = $this->getTeamsPreview();
 | |
| 
 | |
|         // Check for edge cases - should not happen here as we check earlier, but be safe
 | |
|         if ($preview['edge_cases']->isNotEmpty()) {
 | |
|             throw new \Exception('Edge cases detected during execution. This should not happen.');
 | |
|         }
 | |
| 
 | |
|         // Delete teams where user is alone
 | |
|         foreach ($preview['to_delete'] as $team) {
 | |
|             try {
 | |
|                 // The Team model's deleting event will handle cleanup of:
 | |
|                 // - private keys
 | |
|                 // - sources
 | |
|                 // - tags
 | |
|                 // - environment variables
 | |
|                 // - s3 storages
 | |
|                 // - notification settings
 | |
|                 $team->delete();
 | |
|                 $counts['deleted']++;
 | |
|             } catch (\Exception $e) {
 | |
|                 \Log::error("Failed to delete team {$team->id}: ".$e->getMessage());
 | |
|                 throw $e; // Re-throw to trigger rollback
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Transfer ownership for teams where user is owner but not alone
 | |
|         foreach ($preview['to_transfer'] as $item) {
 | |
|             try {
 | |
|                 $team = $item['team'];
 | |
|                 $newOwner = $item['new_owner'];
 | |
| 
 | |
|                 // Update the new owner's role to owner
 | |
|                 $team->members()->updateExistingPivot($newOwner->id, ['role' => 'owner']);
 | |
| 
 | |
|                 // Remove the current user from the team
 | |
|                 $team->members()->detach($this->user->id);
 | |
| 
 | |
|                 $counts['transferred']++;
 | |
|             } catch (\Exception $e) {
 | |
|                 \Log::error("Failed to transfer ownership of team {$item['team']->id}: ".$e->getMessage());
 | |
|                 throw $e; // Re-throw to trigger rollback
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Remove user from teams where they're just a member
 | |
|         foreach ($preview['to_leave'] as $team) {
 | |
|             try {
 | |
|                 $team->members()->detach($this->user->id);
 | |
|                 $counts['left']++;
 | |
|             } catch (\Exception $e) {
 | |
|                 \Log::error("Failed to remove user from team {$team->id}: ".$e->getMessage());
 | |
|                 throw $e; // Re-throw to trigger rollback
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $counts;
 | |
|     }
 | |
| 
 | |
|     private function findNewOwner(Team $team): ?User
 | |
|     {
 | |
|         // Only look for admins as potential new owners
 | |
|         // We don't promote regular members automatically
 | |
|         $otherAdmin = $team->members
 | |
|             ->where('id', '!=', $this->user->id)
 | |
|             ->filter(function ($member) {
 | |
|                 return $member->pivot->role === 'admin';
 | |
|             })
 | |
|             ->first();
 | |
| 
 | |
|         return $otherAdmin;
 | |
|     }
 | |
| 
 | |
|     private function isUserPayingForTeamSubscription(Team $team): bool
 | |
|     {
 | |
|         if (! $team->subscription || ! $team->subscription->stripe_customer_id) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         // In Stripe, we need to check if the customer email matches the user's email
 | |
|         // This would require a Stripe API call to get customer details
 | |
|         // For now, we'll check if the subscription was created by this user
 | |
| 
 | |
|         // Alternative approach: Check if user is the one who initiated the subscription
 | |
|         // We could store this information when the subscription is created
 | |
|         // For safety, we'll assume if there's an active subscription and multiple owners,
 | |
|         // we should treat it as an edge case that needs manual review
 | |
| 
 | |
|         if ($team->subscription->stripe_subscription_id &&
 | |
|             $team->subscription->stripe_invoice_paid) {
 | |
|             // Active subscription exists - we should be cautious
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|     }
 | |
| }
 | 
