910 lines
33 KiB
PHP
910 lines
33 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Organization;
|
|
use App\Models\User;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Migrate User to Organization Command
|
|
*
|
|
* This command safely migrates a User model to an Organization model while preserving
|
|
* all associated relationships, accounts, and data.
|
|
*
|
|
* IMPORTANT: This is a one-way migration that cannot be easily reversed.
|
|
* Always run with --dry-run first to preview changes.
|
|
*
|
|
* The migration process:
|
|
* 1. Creates a new Organization with all User data
|
|
* 2. Updates all polymorphic relationships to point to the new Organization
|
|
* 3. Migrates pivot table relationships (bank management, etc.)
|
|
* 4. Updates permission system references
|
|
* 5. Re-indexes Elasticsearch and clears caches
|
|
*
|
|
* Safety validations prevent migration of:
|
|
* - Super Admin users
|
|
* - Users with critical system permissions
|
|
* - Users with conflicting names/emails
|
|
* - Users currently online
|
|
*
|
|
* @author Claude Code
|
|
* @version 1.0
|
|
*/
|
|
class MigrateUserToOrganizationCommand extends Command
|
|
{
|
|
/**
|
|
* The name and signature of the console command.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $signature = 'migrate:user-to-organization {user_id} {--dry-run : Preview changes without executing them}';
|
|
|
|
/**
|
|
* The console command description.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $description = 'Migrate a User model to an Organization model while preserving all relationships and data';
|
|
|
|
/**
|
|
* Execute the console command.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function handle()
|
|
{
|
|
$userId = $this->argument('user_id');
|
|
$dryRun = $this->option('dry-run');
|
|
|
|
if ($dryRun) {
|
|
$this->info('DRY RUN MODE - No changes will be made');
|
|
$this->newLine();
|
|
}
|
|
|
|
// Validate user exists
|
|
$user = User::find($userId);
|
|
if (!$user) {
|
|
$this->error("User with ID {$userId} not found");
|
|
return 1;
|
|
}
|
|
|
|
// Safety validations
|
|
$validationResult = $this->validateUserForMigration($user);
|
|
|
|
// Handle blocking errors (cannot proceed)
|
|
if (!empty($validationResult['blocking_errors'])) {
|
|
$this->error("Migration validation failed:");
|
|
foreach ($validationResult['blocking_errors'] as $error) {
|
|
$this->line(" • {$error}");
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// Handle warnings that require confirmation
|
|
if (!empty($validationResult['warnings'])) {
|
|
$this->warn("Migration warnings:");
|
|
foreach ($validationResult['warnings'] as $warning) {
|
|
$this->line(" • {$warning}");
|
|
}
|
|
|
|
if (!$dryRun && !$this->confirm('Do you want to continue with the migration despite these warnings?', false)) {
|
|
$this->info('Migration cancelled by user.');
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Handle specific organization management conflicts
|
|
if (!empty($validationResult['organization_conflicts'])) {
|
|
$this->warn("Organization Management Conflicts:");
|
|
foreach ($validationResult['organization_conflicts'] as $conflict) {
|
|
$this->line(" • {$conflict}");
|
|
}
|
|
|
|
if (!$dryRun && !$this->confirm('Do you still want to migrate? All organization management relationships will be removed.', false)) {
|
|
$this->info('Migration cancelled by user.');
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
$this->info("Migrating User '{$user->name}' (ID: {$userId}) to Organization");
|
|
$this->newLine();
|
|
|
|
if ($dryRun) {
|
|
return $this->previewMigration($user);
|
|
}
|
|
|
|
return $this->executeMigration($user);
|
|
}
|
|
|
|
/**
|
|
* Preview what the migration would do
|
|
*/
|
|
private function previewMigration(User $user): int
|
|
{
|
|
$this->info('MIGRATION PREVIEW:');
|
|
$this->line('─────────────────────');
|
|
|
|
// Check what would be created
|
|
$this->info("Would create Organization:");
|
|
$this->line(" Name: {$user->name}");
|
|
$this->line(" Email: {$user->email}");
|
|
$this->line(" Limits: min=" . timebank_config('accounts.organization.limit_min', 0) .
|
|
", max=" . timebank_config('accounts.organization.limit_max', 12000));
|
|
|
|
// Show admin cleanup that would happen
|
|
$adminRoles = ['admin', 'super-admin', 'Admin', 'Super Admin'];
|
|
$hasAdminRoles = false;
|
|
foreach ($adminRoles as $roleName) {
|
|
if ($user->hasRole($roleName)) {
|
|
if (!$hasAdminRoles) {
|
|
$this->line("Would remove admin roles:");
|
|
$hasAdminRoles = true;
|
|
}
|
|
$this->line(" - {$roleName}");
|
|
}
|
|
}
|
|
|
|
$adminCount = $user->admins()->count();
|
|
if ($adminCount > 0) {
|
|
$this->line("Would remove {$adminCount} admin relationships");
|
|
}
|
|
|
|
// Show bank management cleanup that would happen
|
|
$bankCount = $user->banksManaged()->count();
|
|
if ($bankCount > 0) {
|
|
$bankNames = $user->banksManaged()->pluck('name')->toArray();
|
|
$this->line("Would remove management of {$bankCount} bank(s):");
|
|
foreach ($bankNames as $bankName) {
|
|
$this->line(" - {$bankName}");
|
|
}
|
|
}
|
|
|
|
// Show organization management cleanup that would happen
|
|
$organizationCount = $user->organizations()->count();
|
|
if ($organizationCount > 0) {
|
|
$orgNames = $user->organizations()->pluck('name')->toArray();
|
|
$this->line("Would remove management of {$organizationCount} organization(s):");
|
|
foreach ($orgNames as $orgName) {
|
|
$this->line(" - {$orgName}");
|
|
}
|
|
}
|
|
|
|
// Check polymorphic relationships
|
|
$polymorphicTables = $this->getPolymorphicTables();
|
|
foreach ($polymorphicTables as $table => $columns) {
|
|
$count = DB::table($table)
|
|
->where($columns['type'], 'App\Models\User')
|
|
->where($columns['id'], $user->id)
|
|
->count();
|
|
|
|
if ($count > 0) {
|
|
$this->line("{$table}: {$count} records to update");
|
|
}
|
|
}
|
|
|
|
// Check pivot tables
|
|
$pivotTables = $this->getPivotTables();
|
|
foreach ($pivotTables as $table => $column) {
|
|
$count = DB::table($table)->where($column, $user->id)->count();
|
|
if ($count > 0) {
|
|
$this->line("{$table}: {$count} records to migrate");
|
|
}
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info('Run without --dry-run to execute the migration');
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Execute the actual migration
|
|
*/
|
|
private function executeMigration(User $user): int
|
|
{
|
|
DB::beginTransaction();
|
|
|
|
try {
|
|
$this->info('Starting migration...');
|
|
|
|
// Step 1: Remove admin relationships and roles
|
|
$this->removeAdminRelationshipsAndRoles($user);
|
|
|
|
// Step 2: Create Organization
|
|
$organization = $this->createOrganizationFromUser($user);
|
|
$this->info("Created Organization with ID: {$organization->id}");
|
|
|
|
// Step 3: Update polymorphic relationships
|
|
$this->updatePolymorphicRelationships($user, $organization);
|
|
|
|
// Step 4: Update love package relationships
|
|
$this->updateLoveRelationships($user, $organization);
|
|
|
|
// Step 5: Handle pivot tables
|
|
$this->updatePivotTables($user, $organization);
|
|
|
|
// Step 6: Update direct references
|
|
$this->updateDirectReferences($user, $organization);
|
|
|
|
// Step 7: Handle special cases
|
|
$this->handleSpecialCases($user, $organization);
|
|
|
|
// Step 8: Delete the original User and cleanup relationships
|
|
$this->deleteUserAndRelationships($user, $organization);
|
|
|
|
DB::commit();
|
|
|
|
$this->newLine();
|
|
$this->info('Migration completed successfully!');
|
|
$this->info("User ID {$user->id} is now Organization ID {$organization->id}");
|
|
$this->info('Original User record and all relationships deleted');
|
|
|
|
return 0;
|
|
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
$this->error('Migration failed: ' . $e->getMessage());
|
|
Log::error('User to Organization migration failed', [
|
|
'user_id' => $user->id,
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create Organization from User data
|
|
*/
|
|
private function createOrganizationFromUser(User $user): Organization
|
|
{
|
|
// Copy all common columns between User and Organization models
|
|
$organizationData = $user->only([
|
|
'name', 'full_name', 'email', 'profile_photo_path',
|
|
'about', 'about_short', 'motivation', 'website',
|
|
'phone', 'phone_public', 'password', 'lang_preference',
|
|
'last_login_at', 'last_login_ip', 'comment', 'cyclos_id',
|
|
'cyclos_salt', 'cyclos_skills', 'email_verified_at',
|
|
'inactive_at', 'deleted_at'
|
|
]);
|
|
|
|
// Set Organization-specific limits from config
|
|
$organizationData['limit_min'] = timebank_config('profiles.organization.limit_min', 0);
|
|
$organizationData['limit_max'] = timebank_config('profiles.organization.limit_max', 6000);
|
|
|
|
// Copy timestamps
|
|
$organizationData['created_at'] = $user->created_at;
|
|
$organizationData['updated_at'] = $user->updated_at;
|
|
|
|
// Create the organization with fillable fields (don't include love IDs yet)
|
|
$organization = Organization::create($organizationData);
|
|
|
|
// Set non-fillable fields directly on the model to bypass mass assignment protection
|
|
$nonFillableFields = [
|
|
'comment', 'cyclos_id', 'cyclos_salt', 'cyclos_skills',
|
|
'email_verified_at', 'inactive_at', 'deleted_at'
|
|
];
|
|
|
|
foreach ($nonFillableFields as $field) {
|
|
if ($user->$field !== null) {
|
|
$organization->$field = $user->$field;
|
|
}
|
|
}
|
|
|
|
// Force copy love package IDs from user AFTER organization creation
|
|
// This ensures we preserve the original user's love IDs, not newly generated ones
|
|
if ($user->love_reactant_id) {
|
|
$organization->love_reactant_id = $user->love_reactant_id;
|
|
}
|
|
if ($user->love_reacter_id) {
|
|
$organization->love_reacter_id = $user->love_reacter_id;
|
|
}
|
|
|
|
// Save the organization with all additional fields including love IDs
|
|
$organization->save();
|
|
|
|
return $organization;
|
|
}
|
|
|
|
/**
|
|
* Update all polymorphic relationships
|
|
*/
|
|
private function updatePolymorphicRelationships(User $user, Organization $organization): void
|
|
{
|
|
$this->info('Updating polymorphic relationships...');
|
|
|
|
$polymorphicTables = $this->getPolymorphicTables();
|
|
|
|
foreach ($polymorphicTables as $table => $columns) {
|
|
$count = DB::table($table)
|
|
->where($columns['type'], 'App\Models\User')
|
|
->where($columns['id'], $user->id)
|
|
->update([
|
|
$columns['type'] => 'App\Models\Organization',
|
|
$columns['id'] => $organization->id
|
|
]);
|
|
|
|
if ($count > 0) {
|
|
$this->line(" {$table}: Updated {$count} records");
|
|
}
|
|
}
|
|
|
|
// Update account limits specifically
|
|
$this->updateAccountLimits($organization);
|
|
}
|
|
|
|
/**
|
|
* Update account limits to organization values
|
|
*/
|
|
private function updateAccountLimits(Organization $organization): void
|
|
{
|
|
$this->info('Updating account limits and names...');
|
|
|
|
$userAccountName = timebank_config('accounts.user.name', 'personal');
|
|
$orgAccountName = timebank_config('accounts.organization.name', 'organization');
|
|
$limitMin = timebank_config('accounts.organization.limit_min', 0);
|
|
$limitMax = timebank_config('accounts.organization.limit_max', 12000);
|
|
|
|
// Get all accounts for this organization
|
|
$accounts = DB::table('accounts')
|
|
->where('accountable_type', 'App\Models\Organization')
|
|
->where('accountable_id', $organization->id)
|
|
->get(['id', 'name']);
|
|
|
|
$limitsUpdated = 0;
|
|
$namesRenamed = 0;
|
|
$assignedNames = []; // Track names assigned during this migration
|
|
|
|
foreach ($accounts as $account) {
|
|
$updateData = [
|
|
'limit_min' => $limitMin,
|
|
'limit_max' => $limitMax
|
|
];
|
|
|
|
// Rename accounts based on their current name
|
|
if ($account->name === $userAccountName) {
|
|
// Rename 'personal' to 'organization' (with numbering if needed)
|
|
$newName = $this->generateUniqueAccountName($organization, $orgAccountName, $assignedNames);
|
|
$updateData['name'] = $newName;
|
|
$assignedNames[] = $newName; // Track this assigned name
|
|
$namesRenamed++;
|
|
$this->line(" Renaming account ID {$account->id}: '{$account->name}' → '{$newName}'");
|
|
} elseif ($account->name === 'gift') {
|
|
// Rename 'gift' to 'donation' (with numbering if needed)
|
|
$newName = $this->generateUniqueAccountName($organization, 'donation', $assignedNames);
|
|
$updateData['name'] = $newName;
|
|
$assignedNames[] = $newName; // Track this assigned name
|
|
$namesRenamed++;
|
|
$this->line(" Renaming account ID {$account->id}: '{$account->name}' → '{$newName}'");
|
|
}
|
|
|
|
// Update the account
|
|
DB::table('accounts')
|
|
->where('id', $account->id)
|
|
->update($updateData);
|
|
|
|
$limitsUpdated++;
|
|
}
|
|
|
|
if ($limitsUpdated > 0) {
|
|
$this->line(" Updated limits for {$limitsUpdated} account(s): min={$limitMin}, max={$limitMax}");
|
|
}
|
|
|
|
if ($namesRenamed > 0) {
|
|
$this->line(" Renamed {$namesRenamed} account(s) (personal→organization, gift→donation)");
|
|
}
|
|
|
|
if ($limitsUpdated === 0) {
|
|
$this->line(" No accounts found to update");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a unique account name for the organization
|
|
*/
|
|
private function generateUniqueAccountName(Organization $organization, string $baseName, array $accountsBeingRenamed = []): string
|
|
{
|
|
// Get all existing account names for this organization
|
|
$existingNames = DB::table('accounts')
|
|
->where('accountable_type', 'App\Models\Organization')
|
|
->where('accountable_id', $organization->id)
|
|
->pluck('name')
|
|
->toArray();
|
|
|
|
// Also consider names that are being assigned in this migration batch
|
|
$existingNames = array_merge($existingNames, $accountsBeingRenamed);
|
|
|
|
// If base name doesn't exist, use it
|
|
if (!in_array($baseName, $existingNames)) {
|
|
return $baseName;
|
|
}
|
|
|
|
// Try numbered versions until we find one that doesn't exist
|
|
$counter = 2;
|
|
while (true) {
|
|
$candidateName = $baseName . ' ' . $counter;
|
|
if (!in_array($candidateName, $existingNames)) {
|
|
return $candidateName;
|
|
}
|
|
$counter++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update love package relationships
|
|
*/
|
|
private function updateLoveRelationships(User $user, Organization $organization): void
|
|
{
|
|
$this->info('Updating love package relationships...');
|
|
|
|
// Update love_reactants type from User to Organization
|
|
if ($user->loveReactant) {
|
|
$updated = DB::table('love_reactants')
|
|
->where('id', $user->loveReactant->id)
|
|
->update(['type' => 'App\Models\Organization']);
|
|
|
|
if ($updated > 0) {
|
|
$this->line(" love_reactants: Updated type for reactant ID {$user->loveReactant->id}");
|
|
}
|
|
}
|
|
|
|
// Update love_reacters type from User to Organization
|
|
if ($user->loveReacter) {
|
|
$updated = DB::table('love_reacters')
|
|
->where('id', $user->loveReacter->id)
|
|
->update(['type' => 'App\Models\Organization']);
|
|
|
|
if ($updated > 0) {
|
|
$this->line(" love_reacters: Updated type for reacter ID {$user->loveReacter->id}");
|
|
}
|
|
}
|
|
|
|
if (!$user->loveReactant && !$user->loveReacter) {
|
|
$this->line(" No love relationships to update");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle pivot table migrations
|
|
*/
|
|
private function updatePivotTables(User $user, Organization $organization): void
|
|
{
|
|
$this->info('Updating pivot tables...');
|
|
|
|
// Handle bank_user -> bank_organization migration
|
|
$bankRelationships = DB::table('bank_user')->where('user_id', $user->id)->get();
|
|
foreach ($bankRelationships as $relationship) {
|
|
// Create new bank_organization relationship
|
|
DB::table('bank_organization')->insertOrIgnore([
|
|
'bank_id' => $relationship->bank_id,
|
|
'organization_id' => $organization->id,
|
|
'created_at' => $relationship->created_at ?? now(),
|
|
'updated_at' => $relationship->updated_at ?? now()
|
|
]);
|
|
}
|
|
|
|
if ($bankRelationships->count() > 0) {
|
|
$this->line(" bank_organization: Migrated {$bankRelationships->count()} relationships");
|
|
// Delete old bank_user relationships
|
|
DB::table('bank_user')->where('user_id', $user->id)->delete();
|
|
}
|
|
|
|
// Make the User a manager of the new Organization
|
|
DB::table('organization_user')->insertOrIgnore([
|
|
'organization_id' => $organization->id,
|
|
'user_id' => $user->id,
|
|
'created_at' => now(),
|
|
'updated_at' => now()
|
|
]);
|
|
$this->line(" organization_user: Added User as manager");
|
|
|
|
// Handle other pivot tables
|
|
$pivotTables = $this->getPivotTables();
|
|
foreach ($pivotTables as $table => $column) {
|
|
if ($table === 'bank_user') {
|
|
continue;
|
|
} // Already handled above
|
|
|
|
$count = DB::table($table)->where($column, $user->id)->count();
|
|
if ($count > 0) {
|
|
$this->line(" {$table}: {$count} records need manual review");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update direct references
|
|
*/
|
|
private function updateDirectReferences(User $user, Organization $organization): void
|
|
{
|
|
$this->info('Updating direct references...');
|
|
|
|
// Handle Spatie Permission tables
|
|
DB::table('model_has_roles')
|
|
->where('model_type', 'App\Models\User')
|
|
->where('model_id', $user->id)
|
|
->update([
|
|
'model_type' => 'App\Models\Organization',
|
|
'model_id' => $organization->id
|
|
]);
|
|
|
|
DB::table('model_has_permissions')
|
|
->where('model_type', 'App\Models\User')
|
|
->where('model_id', $user->id)
|
|
->update([
|
|
'model_type' => 'App\Models\Organization',
|
|
'model_id' => $organization->id
|
|
]);
|
|
|
|
$this->line(" Updated permission system references");
|
|
}
|
|
|
|
/**
|
|
* Handle special cases like Elasticsearch, caches, etc.
|
|
*/
|
|
private function handleSpecialCases(User $user, Organization $organization): void
|
|
{
|
|
$this->info('Handling special cases...');
|
|
|
|
// Re-index in Elasticsearch
|
|
try {
|
|
$organization->searchable();
|
|
$this->line(" Updated Elasticsearch index");
|
|
} catch (\Exception $e) {
|
|
$this->line(" Elasticsearch update failed: " . $e->getMessage());
|
|
}
|
|
|
|
// Clear caches
|
|
if (function_exists('cache')) {
|
|
cache()->forget("user.{$user->id}");
|
|
$this->line(" Cleared user cache");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get polymorphic table mappings
|
|
*/
|
|
private function getPolymorphicTables(): array
|
|
{
|
|
$tables = [
|
|
'accounts' => ['type' => 'accountable_type', 'id' => 'accountable_id'],
|
|
'locations' => ['type' => 'locatable_type', 'id' => 'locatable_id'],
|
|
'posts' => ['type' => 'postable_type', 'id' => 'postable_id'],
|
|
'categories' => ['type' => 'categoryable_type', 'id' => 'categoryable_id'],
|
|
'activity_log' => ['type' => 'subject_type', 'id' => 'subject_id'],
|
|
];
|
|
|
|
// Check for optional tables that might exist
|
|
if ($this->tableExists('languagables')) {
|
|
$tables['languagables'] = ['type' => 'languagable_type', 'id' => 'languagable_id'];
|
|
}
|
|
|
|
if ($this->tableExists('sociables')) {
|
|
$tables['sociables'] = ['type' => 'sociable_type', 'id' => 'sociable_id'];
|
|
}
|
|
|
|
if ($this->tableExists('bank_clients')) {
|
|
$tables['bank_clients'] = ['type' => 'client_type', 'id' => 'client_id'];
|
|
}
|
|
|
|
// Love package tables are handled separately in updateLoveRelationships()
|
|
// as they use a different pattern (type column instead of polymorphic columns)
|
|
|
|
return $tables;
|
|
}
|
|
|
|
/**
|
|
* Get pivot table mappings
|
|
*/
|
|
private function getPivotTables(): array
|
|
{
|
|
return [
|
|
'bank_user' => 'user_id',
|
|
'admin_user' => 'user_id',
|
|
'organization_user' => 'user_id',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Check if a table exists
|
|
*/
|
|
private function tableExists(string $tableName): bool
|
|
{
|
|
try {
|
|
DB::table($tableName)->limit(1)->count();
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate if user can be safely migrated
|
|
*/
|
|
private function validateUserForMigration(User $user): array
|
|
{
|
|
$blockingErrors = [];
|
|
$warnings = [];
|
|
|
|
// BLOCKING ERRORS - Migration cannot proceed
|
|
|
|
// Check if an organization with the same name already exists
|
|
if (Organization::where('name', $user->name)->exists()) {
|
|
$blockingErrors[] = "An Organization with name '{$user->name}' already exists";
|
|
}
|
|
|
|
// Check if an organization with the same email already exists
|
|
if (Organization::where('email', $user->email)->exists()) {
|
|
$blockingErrors[] = "An Organization with email '{$user->email}' already exists";
|
|
}
|
|
|
|
// Check if user is currently online (has recent presence)
|
|
try {
|
|
if (method_exists($user, 'isOnline') && $user->isOnline()) {
|
|
$blockingErrors[] = "User is currently online - wait for them to go offline before migrating";
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Skip online check if presence system is not available
|
|
}
|
|
|
|
// WARNINGS - Migration can proceed with confirmation
|
|
|
|
// Check if user is a super admin or critical system user
|
|
if ($user->hasRole('Super Admin') || $user->hasRole('super-admin')) {
|
|
$warnings[] = "User has Super Admin role - all admin privileges will be removed during migration";
|
|
}
|
|
|
|
// Check if user has critical system permissions
|
|
$criticalPermissions = ['manage system', 'manage users', 'super-admin'];
|
|
foreach ($criticalPermissions as $permission) {
|
|
if ($user->can($permission)) {
|
|
$warnings[] = "User has critical permission '{$permission}' - all admin permissions will be removed during migration";
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check if user has active two-factor authentication
|
|
if (!empty($user->two_factor_secret)) {
|
|
$warnings[] = "User has two-factor authentication enabled - this will be lost during migration";
|
|
}
|
|
|
|
// ORGANIZATION CONFLICTS - Separate from general warnings
|
|
$organizationConflicts = [];
|
|
|
|
// Check if user is already managing banks
|
|
if ($user->banksManaged()->count() > 0) {
|
|
$organizationConflicts[] = "User is managing " . $user->banksManaged()->count() . " bank(s). After the migration this will not be possible any more.";
|
|
}
|
|
|
|
// Check if user is already managing organizations
|
|
if ($user->organizations()->count() > 0) {
|
|
$organizationConflicts[] = "User is managing " . $user->organizations()->count() . " organization(s). After the migration this will not be possible any more.";
|
|
}
|
|
|
|
return [
|
|
'blocking_errors' => $blockingErrors,
|
|
'warnings' => $warnings,
|
|
'organization_conflicts' => $organizationConflicts
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Remove admin relationships and roles before migration
|
|
*/
|
|
private function removeAdminRelationshipsAndRoles(User $user): void
|
|
{
|
|
$this->info('Removing admin relationships and roles...');
|
|
|
|
// Remove admin roles using Spatie method
|
|
$adminRoles = ['admin', 'super-admin', 'Admin', 'Super Admin'];
|
|
$rolesRemoved = [];
|
|
|
|
foreach ($adminRoles as $roleName) {
|
|
if ($user->hasRole($roleName)) {
|
|
$user->removeRole($roleName);
|
|
$rolesRemoved[] = $roleName;
|
|
}
|
|
}
|
|
|
|
if (!empty($rolesRemoved)) {
|
|
$this->line(' Removed roles: ' . implode(', ', $rolesRemoved));
|
|
}
|
|
|
|
// Remove admin relationships (many-to-many)
|
|
$adminCount = $user->admins()->count();
|
|
if ($adminCount > 0) {
|
|
$user->admins()->detach();
|
|
$this->line(" Removed {$adminCount} admin relationships");
|
|
}
|
|
|
|
// Remove bank management relationships
|
|
$bankCount = $user->banksManaged()->count();
|
|
if ($bankCount > 0) {
|
|
$bankNames = $user->banksManaged()->pluck('name')->toArray();
|
|
$user->banksManaged()->detach(); // Un-associate all managed banks
|
|
$this->line(" Removed management of {$bankCount} bank(s): " . implode(', ', $bankNames));
|
|
}
|
|
|
|
// Remove organization management relationships
|
|
$organizationCount = $user->organizations()->count();
|
|
if ($organizationCount > 0) {
|
|
$orgNames = $user->organizations()->pluck('name')->toArray();
|
|
$user->organizations()->detach(); // Un-associate all managed organizations
|
|
$this->line(" Removed management of {$organizationCount} organization(s): " . implode(', ', $orgNames));
|
|
}
|
|
|
|
if (empty($rolesRemoved) && $adminCount === 0 && $bankCount === 0 && $organizationCount === 0) {
|
|
$this->line(' No admin relationships or roles to remove');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete User and cleanup all remaining relationships
|
|
*/
|
|
private function deleteUserAndRelationships(User $user, Organization $organization): void
|
|
{
|
|
$this->info('Deleting User and cleaning up relationships...');
|
|
|
|
// Step 1: Verify critical data was migrated
|
|
$this->verifyMigrationCompleteness($user, $organization);
|
|
|
|
// Step 2: Clean up pivot table relationships that weren't migrated
|
|
$this->cleanupPivotRelationships($user);
|
|
|
|
// Step 3: Clean up remaining foreign key references
|
|
$this->cleanupForeignKeyReferences($user);
|
|
|
|
// Step 4: Delete the User model
|
|
$userId = $user->id;
|
|
$userName = $user->name;
|
|
|
|
$user->delete();
|
|
|
|
$this->line(" Deleted User '{$userName}' (ID: {$userId})");
|
|
|
|
// Step 5: Verify complete deletion
|
|
$this->verifyUserDeletion($userId);
|
|
}
|
|
|
|
/**
|
|
* Verify that critical data was successfully migrated
|
|
*/
|
|
private function verifyMigrationCompleteness(User $user, Organization $organization): void
|
|
{
|
|
// Check that accounts were transferred
|
|
$userAccounts = $user->accounts()->count();
|
|
$orgAccounts = $organization->accounts()->count();
|
|
|
|
if ($userAccounts > 0) {
|
|
throw new \Exception("User still has {$userAccounts} accounts - migration incomplete");
|
|
}
|
|
|
|
if ($orgAccounts === 0) {
|
|
$this->line(" Organization has no accounts - this may be expected");
|
|
}
|
|
|
|
$this->line(" Migration verification passed");
|
|
}
|
|
|
|
/**
|
|
* Clean up pivot table relationships
|
|
*/
|
|
private function cleanupPivotRelationships(User $user): void
|
|
{
|
|
$cleanupTables = [
|
|
'organization_user' => 'user_id',
|
|
'bank_user' => 'user_id'
|
|
// admin_user is already handled in removeAdminRelationshipsAndRoles()
|
|
];
|
|
|
|
foreach ($cleanupTables as $table => $column) {
|
|
if ($this->tableExists($table)) {
|
|
$count = DB::table($table)->where($column, $user->id)->count();
|
|
if ($count > 0) {
|
|
DB::table($table)->where($column, $user->id)->delete();
|
|
$this->line(" Cleaned up {$table}: {$count} records deleted");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up Spatie permission pivot tables
|
|
$permissionTables = [
|
|
'model_has_roles' => ['model_type' => 'App\Models\User', 'model_id' => $user->id],
|
|
'model_has_permissions' => ['model_type' => 'App\Models\User', 'model_id' => $user->id]
|
|
];
|
|
|
|
foreach ($permissionTables as $table => $conditions) {
|
|
if ($this->tableExists($table)) {
|
|
$count = DB::table($table)->where($conditions)->count();
|
|
if ($count > 0) {
|
|
DB::table($table)->where($conditions)->delete();
|
|
$this->line(" Cleaned up {$table}: {$count} records deleted");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up foreign key references
|
|
*/
|
|
private function cleanupForeignKeyReferences(User $user): void
|
|
{
|
|
// Clean up activity logs where User is the causer (not subject - those are audit trail)
|
|
if ($this->tableExists('activity_log')) {
|
|
$count = DB::table('activity_log')
|
|
->where('causer_type', 'App\Models\User')
|
|
->where('causer_id', $user->id)
|
|
->count();
|
|
|
|
if ($count > 0) {
|
|
// Set causer to null instead of deleting logs for audit trail
|
|
DB::table('activity_log')
|
|
->where('causer_type', 'App\Models\User')
|
|
->where('causer_id', $user->id)
|
|
->update([
|
|
'causer_type' => null,
|
|
'causer_id' => null
|
|
]);
|
|
$this->line(" Cleaned up activity_log causers: {$count} records updated");
|
|
}
|
|
}
|
|
|
|
// Love package cleanup is handled by updateLoveRelationships() method
|
|
// No additional cleanup needed as we're updating types, not deleting records
|
|
|
|
// Clean up any remaining chat/messaging relationships
|
|
$chatTables = ['chat_participants', 'chat_messages'];
|
|
foreach ($chatTables as $table) {
|
|
if ($this->tableExists($table)) {
|
|
$userColumn = $table === 'chat_participants' ? 'user_id' : 'sender_id';
|
|
$count = DB::table($table)->where($userColumn, $user->id)->count();
|
|
|
|
if ($count > 0) {
|
|
if ($table === 'chat_messages') {
|
|
// For messages, mark as deleted rather than removing for chat history
|
|
DB::table($table)
|
|
->where($userColumn, $user->id)
|
|
->update(['sender_id' => null, 'deleted_at' => now()]);
|
|
$this->line(" Cleaned up {$table}: {$count} records marked as deleted");
|
|
} else {
|
|
DB::table($table)->where($userColumn, $user->id)->delete();
|
|
$this->line(" Cleaned up {$table}: {$count} records deleted");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify User was completely deleted
|
|
*/
|
|
private function verifyUserDeletion(int $userId): void
|
|
{
|
|
// Check that User record is gone
|
|
if (User::find($userId)) {
|
|
throw new \Exception("User deletion failed - User {$userId} still exists");
|
|
}
|
|
|
|
// Check for any remaining references in key tables
|
|
$checkTables = [
|
|
'organization_user' => 'user_id',
|
|
'bank_user' => 'user_id',
|
|
'admin_user' => 'user_id'
|
|
];
|
|
|
|
foreach ($checkTables as $table => $column) {
|
|
if ($this->tableExists($table)) {
|
|
$remaining = DB::table($table)->where($column, $userId)->count();
|
|
if ($remaining > 0) {
|
|
$this->line(" Warning: {$remaining} records remain in {$table}");
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->line(" User deletion verification completed");
|
|
}
|
|
}
|