Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,493 @@
<?php
namespace App\Console\Commands;
use App\Models\Bank;
use App\Models\Locations\Location;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
class MigrateCyclosProfilesCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrate:cyclos-profiles {source_db? : Name of the source Cyclos database (skips prompt if provided)}';
protected $description = 'Migrates the Cyclos profile contents from the old Cyclos database to the new Laravel database';
public function handle()
{
// Use argument if provided, otherwise fall back to cache (set by migrate:cyclos during db:seed)
$sourceDb = $this->argument('source_db') ?: cache()->get('cyclos_migration_source_db');
if (empty($sourceDb)) {
// If not in cache, ask for it
$this->info('The source Cyclos database should be imported into MySQL and accessible from this application.');
$this->info('Hint: Place the database dump in the app root and import with: mysql -u root -p < cyclos_dump.sql');
$sourceDb = $this->ask('Enter the name of the source Cyclos database');
if (empty($sourceDb)) {
$this->error('Source database name is required.');
return 1;
}
// Remove .sql extension if present
if (str_ends_with(strtolower($sourceDb), '.sql')) {
$sourceDb = substr($sourceDb, 0, -4);
$this->info("Using database name: {$sourceDb}");
}
// Verify the database exists
$databases = DB::select('SHOW DATABASES');
$databaseNames = array_map(fn($db) => $db->Database, $databases);
if (!in_array($sourceDb, $databaseNames)) {
$this->error("Database '{$sourceDb}' does not exist.");
$this->info('Available databases:');
foreach ($databaseNames as $name) {
if (!in_array($name, ['information_schema', 'mysql', 'performance_schema', 'sys'])) {
$this->line(" - {$name}");
}
}
return 1;
}
} else {
$this->info("Using source database from previous step: {$sourceDb}");
}
$destinationDb = env('DB_DATABASE');
// Migrate phone field
$tables = ['users', 'organizations', 'banks'];
foreach ($tables as $tableName) {
DB::beginTransaction();
try {
$affectedRows = DB::affectingStatement("
UPDATE `{$destinationDb}`.`{$tableName}` dest
JOIN (
SELECT
c.member_id,
LEFT(c.string_value, 20) AS phone -- Truncate to 20 characters
FROM `{$sourceDb}`.`custom_field_values` c
WHERE c.field_id = 7
) src ON dest.cyclos_id = src.member_id
SET dest.phone = src.phone
");
DB::commit();
$this->info(ucfirst($tableName) . " phone field updated for $affectedRows records");
} catch (\Exception $e) {
DB::rollBack();
$this->error(ucfirst($tableName) . " phone field migration failed: " . $e->getMessage());
}
}
// Migrate locations
$countryCodeMap = [
860 => 2, // BE
861 => 7, // PT
862 => 1, // NL
863 => 10, // country not set / other country → "Location not specified"
];
$cityCodeMap = [
864 => 188, // Amsterdam
865 => 200, // Haarlem
866 => 316, // Leiden
867 => 305, // The Hague
868 => 300, // Delft
869 => 331, // Rotterdam
870 => 272, // Utrecht
881 => 345, // Brussels
];
$updatedRecordsCount = 0;
DB::beginTransaction();
try {
// Wrap the migration calls in a function that returns the count of updated records
$updatedRecordsCount += $this->migrateLocationData('User', $destinationDb, $sourceDb, $countryCodeMap, $cityCodeMap);
$updatedRecordsCount += $this->migrateLocationData('Organization', $destinationDb, $sourceDb, $countryCodeMap, $cityCodeMap);
$updatedRecordsCount += $this->migrateLocationData('Bank', $destinationDb, $sourceDb, $countryCodeMap, $cityCodeMap);
DB::commit();
// Output the total number of records updated
$this->info("Location fields migration updated for: " . $updatedRecordsCount);
} catch (\Exception $e) {
DB::rollBack();
$this->error("Location fields migration failed: " . $e->getMessage());
}
// Migrate user about field
$tables = ['users'];
foreach ($tables as $tableName) {
DB::beginTransaction();
try {
$affectedRows = DB::affectingStatement("
UPDATE `{$destinationDb}`.`{$tableName}` dest
JOIN (
SELECT
c.member_id,
c.string_value AS about
FROM `{$sourceDb}`.`custom_field_values` c
WHERE c.field_id = 17
) src ON dest.cyclos_id = src.member_id
SET dest.about = src.about
");
DB::commit();
$this->info(ucfirst($tableName) . " about field updated for $affectedRows records");
} catch (\Exception $e) {
DB::rollBack();
$this->error(ucfirst($tableName) . " about field migration failed: " . $e->getMessage());
}
}
// Migrate motivation field
$tables = ['users', 'organizations', 'banks'];
foreach ($tables as $tableName) {
DB::beginTransaction();
try {
$affectedRows = DB::affectingStatement("
UPDATE `{$destinationDb}`.`{$tableName}` dest
JOIN (
SELECT
c.member_id,
c.string_value AS motivation
FROM `{$sourceDb}`.`custom_field_values` c
WHERE c.field_id = 35
) src ON dest.cyclos_id = src.member_id
SET dest.motivation = src.motivation
");
DB::commit();
$this->info(ucfirst($tableName) . " motivation field updated for $affectedRows records");
} catch (\Exception $e) {
DB::rollBack();
$this->error(ucfirst($tableName) . " motivation field migration failed: " . $e->getMessage());
}
}
// Migrate website field
$tables = ['users', 'organizations', 'banks'];
foreach ($tables as $tableName) {
DB::beginTransaction();
try {
$affectedRows = DB::affectingStatement("
UPDATE `{$destinationDb}`.`{$tableName}` dest
JOIN (
SELECT
c.member_id,
c.string_value AS website
FROM `{$sourceDb}`.`custom_field_values` c
WHERE c.field_id = 10
) src ON dest.cyclos_id = src.member_id
SET dest.website = src.website
");
DB::commit();
$this->info(ucfirst($tableName) . " website field updated for $affectedRows records");
} catch (\Exception $e) {
DB::rollBack();
$this->error(ucfirst($tableName) . " website field migration failed: " . $e->getMessage());
}
}
// Migrate birthday field
$tables = ['users'];
foreach ($tables as $tableName) {
DB::beginTransaction();
try {
$affectedRows = DB::affectingStatement("
UPDATE `{$destinationDb}`.`{$tableName}` dest
JOIN (
SELECT
c.member_id,
STR_TO_DATE(REPLACE(c.string_value, '/', '-'), '%d-%m-%Y') AS birthday
FROM `{$sourceDb}`.`custom_field_values` c
WHERE c.field_id = 1
) src ON dest.cyclos_id = src.member_id
SET dest.date_of_birth = src.birthday
");
DB::commit();
$this->info(ucfirst($tableName) . " birthday field updated for $affectedRows records");
} catch (\Exception $e) {
DB::rollBack();
$this->error(ucfirst($tableName) . " birthday field migration failed: " . $e->getMessage());
}
}
// Migrate General Newsletter field to message_settings table
DB::beginTransaction();
try {
// Get all newsletter preferences from Cyclos
$newsletterPrefs = DB::table("{$sourceDb}.custom_field_values")
->where('field_id', 28)
->get(['member_id', 'possible_value_id']);
$totalUpdated = 0;
$tables = [
'users' => User::class,
'organizations' => Organization::class,
'banks' => Bank::class
];
foreach ($tables as $tableName => $modelClass) {
foreach ($newsletterPrefs as $pref) {
$entity = DB::table($tableName)
->where('cyclos_id', $pref->member_id)
->first();
if ($entity) {
$model = $modelClass::find($entity->id);
if ($model) {
// Convert: 790 (No) → 0, 791 (Yes) → 1, null → 1
$value = $pref->possible_value_id == 790 ? 0 : 1;
// Update or create message settings
$model->message_settings()->updateOrCreate(
['message_settingable_id' => $model->id, 'message_settingable_type' => $modelClass],
['general_newsletter' => $value]
);
$totalUpdated++;
}
}
}
}
DB::commit();
$this->info("General newsletter field migrated to message_settings for {$totalUpdated} records");
} catch (\Exception $e) {
DB::rollBack();
$this->error("General newsletter field migration failed: " . $e->getMessage());
}
// Migrate Local Newsletter field to message_settings table
DB::beginTransaction();
try {
// Get all newsletter preferences from Cyclos
$newsletterPrefs = DB::table("{$sourceDb}.custom_field_values")
->where('field_id', 29)
->get(['member_id', 'possible_value_id']);
$totalUpdated = 0;
$tables = [
'users' => User::class,
'organizations' => Organization::class,
'banks' => Bank::class
];
foreach ($tables as $tableName => $modelClass) {
foreach ($newsletterPrefs as $pref) {
$entity = DB::table($tableName)
->where('cyclos_id', $pref->member_id)
->first();
if ($entity) {
$model = $modelClass::find($entity->id);
if ($model) {
// Convert: 792 (No) → 0, 793 (Yes) → 1, null → 1
$value = $pref->possible_value_id == 792 ? 0 : 1;
// Update or create message settings
$model->message_settings()->updateOrCreate(
['message_settingable_id' => $model->id, 'message_settingable_type' => $modelClass],
['local_newsletter' => $value]
);
$totalUpdated++;
}
}
}
}
DB::commit();
$this->info("Local newsletter field migrated to message_settings for {$totalUpdated} records");
} catch (\Exception $e) {
DB::rollBack();
$this->error("Local newsletter field migration failed: " . $e->getMessage());
}
// Migrate Cyclos skills from refined database
$tables = ['users', 'organizations'];
foreach ($tables as $tableName) {
DB::beginTransaction();
try {
$affectedRows = DB::affectingStatement("
UPDATE `{$destinationDb}`.`{$tableName}` dest
JOIN (
SELECT
c.member_id,
CASE
WHEN CHAR_LENGTH(c.string_value) > 495
THEN CONCAT(LEFT(c.string_value, 495), ' ...')
ELSE c.string_value
END AS cyclos_skills
FROM `{$sourceDb}`.`custom_field_values` c
WHERE c.field_id = 13
) src ON dest.cyclos_id = src.member_id
SET dest.cyclos_skills = src.cyclos_skills
");
DB::commit();
$this->info(ucfirst($tableName) . " skills field updated for $affectedRows records");
} catch (\Exception $e) {
DB::rollBack();
$this->error(ucfirst($tableName) . " skills field migration failed: " . $e->getMessage());
}
}
// Strip all HTML tags from imported tables
foreach ($tables as $tableName) {
$records = DB::table($tableName)->select('id', 'cyclos_skills')->whereNotNull('cyclos_skills')->get();
foreach ($records as $record) {
$cleaned = strip_tags($record->cyclos_skills);
if ($cleaned !== $record->cyclos_skills) {
DB::table($tableName)->where('id', $record->id)->update(['cyclos_skills' => $cleaned]);
}
}
$records = DB::table($tableName)->select('id', 'about')->whereNotNull('about')->get();
foreach ($records as $record) {
$cleaned = strip_tags($record->about);
if ($cleaned !== $record->about) {
DB::table($tableName)->where('id', $record->id)->update(['about' => $cleaned]);
}
}
}
}
// Set suspicious robot members to inactive
// 1755
// 1768
// 1776
// 1777
// Check if user.about is null, if true, copy skill tags where length > 50 to user.about
// if user.about <> null, copy skill tags where length > 50 to about_short or update this field
protected function migrateLocationData($modelClass, $destinationDb, $sourceDb, $countryCodeMap, $cityCodeMap)
{
$fullyQualifiedModelClass = "App\\Models\\" . $modelClass;
$cyclos_countries = DB::table("{$sourceDb}.custom_field_values")
->where('field_id', 36)
->get(['possible_value_id', 'member_id']);
$cyclos_cities = DB::table("{$sourceDb}.custom_field_values")
->where('field_id', 38)
->get(['possible_value_id', 'member_id']);
$remappedCountries = $cyclos_countries->mapWithKeys(function ($item) use ($countryCodeMap) {
return [$item->member_id => $countryCodeMap[$item->possible_value_id] ?? null];
});
$remappedCities = $cyclos_cities->mapWithKeys(function ($item) use ($cityCodeMap) {
return [$item->member_id => $cityCodeMap[$item->possible_value_id] ?? null];
});
$recordUpdateCount = 0;
$syncedDataCount = 0;
foreach ($remappedCountries as $memberId => $countryId) {
$cityId = $remappedCities[$memberId] ?? null;
if ($countryId !== null || $cityId !== null) {
$entity = DB::table("{$destinationDb}." . strtolower($modelClass) . "s")
->where('cyclos_id', $memberId)
->first();
if ($entity) {
$entityModel = $fullyQualifiedModelClass::find($entity->id);
if ($entityModel) {
$location = new Location();
$location->name = 'Default location';
$location->country_id = $countryId;
$location->city_id = $cityId;
$entityModel->locations()->save($location);
$recordUpdateCount++;
// Sync all missing location data (divisions, etc.)
try {
$synced = $location->syncAllLocationData();
if (!empty($synced)) {
$syncedDataCount++;
$this->info(" → Synced data for {$modelClass} ID {$entity->id}: " . implode(', ', $synced));
}
} catch (\Exception $e) {
$this->warn(" → Failed to sync location data for {$modelClass} ID {$entity->id}: " . $e->getMessage());
}
}
}
}
}
$this->info("{$modelClass}: {$recordUpdateCount} locations created, {$syncedDataCount} had additional data synced");
return $recordUpdateCount;
}
/**
* Tinker script to clean 'about' field containing only a single double quote
*
* Run this in Laravel Tinker:
* php artisan tinker
* Then paste this code
*/
protected function cleanAboutField()
{
echo "Starting cleanup of 'about' fields containing only double quotes...\n\n";
$models = [
'App\Models\User' => 'Users',
'App\Models\Organization' => 'Organizations',
'App\Models\Bank' => 'Banks',
'App\Models\Admin' => 'Admins'
];
$totalUpdated = 0;
foreach ($models as $modelClass => $tableName) {
echo "Processing {$tableName}...\n";
// Check if the model class exists
if (!class_exists($modelClass)) {
echo " - Model {$modelClass} not found, skipping\n";
continue;
}
try {
// Find records where about field contains only a double quote
$records = $modelClass::where('about', '"')->get();
echo " - Found {$records->count()} records with about = '\"'\n";
if ($records->count() > 0) {
// Update records to set about to null
$updated = $modelClass::where('about', '"')->update(['about' => null]);
echo " - Updated {$updated} records\n";
$totalUpdated += $updated;
}
} catch (\Exception $e) {
echo " - Error processing {$tableName}: " . $e->getMessage() . "\n";
}
echo "\n";
}
echo "Cleanup completed!\n";
echo "Total records updated: {$totalUpdated}\n";
echo "\nTo verify the cleanup, you can run:\n";
foreach ($models as $modelClass => $tableName) {
if (class_exists($modelClass)) {
echo "{$modelClass}::where('about', '\"')->count(); // Should return 0\n";
}
}
}
}