feat: implement comprehensive migration commands for legacy data
- Added migration commands for users, employees, outlets, categories, products, bottles, and perfumes to transfer data from the old database to the new structure. - Each command includes options for forced migration and recreation of existing records. - Implemented progress feedback and error handling during migration processes. - Updated database configuration to include connection settings for the old database. - Enhanced DatabaseSeeder to automatically trigger the migration process after seeding essential data.
This commit is contained in:
parent
6c0db0af4e
commit
3a05ee2617
167
app/Console/Commands/Migrations/MigrateAllFromOldData.php
Normal file
167
app/Console/Commands/Migrations/MigrateAllFromOldData.php
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Migrations;
|
||||||
|
|
||||||
|
use Database\Seeders\FormulaSeeder;
|
||||||
|
use Database\Seeders\UserSeeder;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class MigrateAllFromOldData extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'migrate:all {--force : Force migration without confirmation} {--skip-confirm : Skip individual command confirmations}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Run all data migrations from old database to new structure';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if (! $this->option('force') && ! $this->confirm('Are you sure you want to run ALL migrations? This will add data to your database.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('🚀 Starting complete data migration from old database...');
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
$commands = [
|
||||||
|
['command' => 'migrate:outlets', 'name' => 'Outlets', 'options' => ['--recreate' => true]],
|
||||||
|
['command' => 'migrate:users', 'name' => 'Users'],
|
||||||
|
['command' => 'migrate:employees', 'name' => 'Employees'],
|
||||||
|
['command' => 'migrate:categories', 'name' => 'Categories'],
|
||||||
|
['command' => 'migrate:products', 'name' => 'Products', 'options' => ['--recreate' => true]],
|
||||||
|
['command' => 'migrate:perfumes', 'name' => 'Perfumes', 'options' => ['--recreate' => true]],
|
||||||
|
['command' => 'migrate:bottles', 'name' => 'Bottles', 'options' => ['--recreate' => true]],
|
||||||
|
];
|
||||||
|
|
||||||
|
$totalCommands = count($commands);
|
||||||
|
$completed = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
foreach ($commands as $commandConfig) {
|
||||||
|
$commandName = $commandConfig['command'];
|
||||||
|
$name = $commandConfig['name'];
|
||||||
|
$options = $commandConfig['options'] ?? [];
|
||||||
|
|
||||||
|
// Always add force option
|
||||||
|
$options['--force'] = $this->option('skip-confirm');
|
||||||
|
|
||||||
|
$this->info("📋 Migrating {$name}...");
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$exitCode = $this->call($commandName, $options);
|
||||||
|
|
||||||
|
if ($exitCode === 0) {
|
||||||
|
$this->info("✅ {$name} migration completed successfully!");
|
||||||
|
$completed++;
|
||||||
|
} else {
|
||||||
|
$this->error("❌ {$name} migration failed!");
|
||||||
|
$failed++;
|
||||||
|
|
||||||
|
if (! $this->confirm("Continue with remaining migrations despite {$name} failure?")) {
|
||||||
|
$this->warn('Migration stopped by user.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("❌ {$name} migration error: {$e->getMessage()}");
|
||||||
|
$failed++;
|
||||||
|
|
||||||
|
if (! $this->confirm("Continue with remaining migrations despite {$name} error?")) {
|
||||||
|
$this->warn('Migration stopped by user.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('🎉 Migration process completed!');
|
||||||
|
$this->info('📊 Summary:');
|
||||||
|
$this->info(" ✅ Completed: {$completed}/{$totalCommands} migrations");
|
||||||
|
if ($failed > 0) {
|
||||||
|
$this->warn(" ❌ Failed: {$failed} migrations");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($completed === $totalCommands) {
|
||||||
|
$this->info('🎊 All migrations completed successfully!');
|
||||||
|
|
||||||
|
// Run seeders
|
||||||
|
$this->runSeeders();
|
||||||
|
} else {
|
||||||
|
$this->warn('⚠️ Some migrations failed. Please check the output above for details.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $failed > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the seeders after successful migration
|
||||||
|
*/
|
||||||
|
private function runSeeders()
|
||||||
|
{
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('🌱 Running seeders...');
|
||||||
|
|
||||||
|
$seeders = [
|
||||||
|
['class' => UserSeeder::class, 'name' => 'UserSeeder'],
|
||||||
|
['class' => FormulaSeeder::class, 'name' => 'FormulaSeeder'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$totalSeeders = count($seeders);
|
||||||
|
$completedSeeders = 0;
|
||||||
|
$failedSeeders = 0;
|
||||||
|
|
||||||
|
foreach ($seeders as $seederConfig) {
|
||||||
|
$seederClass = $seederConfig['class'];
|
||||||
|
$name = $seederConfig['name'];
|
||||||
|
|
||||||
|
$this->info("📋 Seeding {$name}...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->call($seederClass);
|
||||||
|
|
||||||
|
$this->info("✅ {$name} seeded successfully!");
|
||||||
|
$completedSeeders++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("❌ {$name} seeding failed: {$e->getMessage()}");
|
||||||
|
$failedSeeders++;
|
||||||
|
|
||||||
|
if (! $this->confirm("Continue with remaining seeders despite {$name} failure?")) {
|
||||||
|
$this->warn('Seeding stopped by user.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('🌱 Seeder process completed!');
|
||||||
|
$this->info('📊 Seeder Summary:');
|
||||||
|
$this->info(" ✅ Completed: {$completedSeeders}/{$totalSeeders} seeders");
|
||||||
|
|
||||||
|
if ($failedSeeders > 0) {
|
||||||
|
$this->warn(" ❌ Failed: {$failedSeeders} seeders");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($completedSeeders === $totalSeeders) {
|
||||||
|
$this->info('🎊 All seeders completed successfully!');
|
||||||
|
} else {
|
||||||
|
$this->warn('⚠️ Some seeders failed. Please check the output above for details.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
158
app/Console/Commands/Migrations/MigrateBottlesFromOldData.php
Normal file
158
app/Console/Commands/Migrations/MigrateBottlesFromOldData.php
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Migrations;
|
||||||
|
|
||||||
|
use App\Models\Bottle;
|
||||||
|
use App\Models\Outlet;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class MigrateBottlesFromOldData extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'migrate:bottles {--force : Force migration without confirmation} {--recreate : Delete existing bottles and recreate}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Migrate bottles data from old database to new structure';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if ($this->option('recreate')) {
|
||||||
|
if (! $this->confirm('Are you sure you want to DELETE ALL EXISTING BOTTLES and recreate them? This action cannot be undone.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->warn('Deleting all existing bottles...');
|
||||||
|
Bottle::query()->delete(); // This will cascade delete outlet relations
|
||||||
|
$this->info('All bottles deleted.');
|
||||||
|
} elseif (! $this->option('force') && ! $this->confirm('Are you sure you want to migrate bottles data? This will add new bottles to your database.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Starting bottles migration...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$oldBottles = DB::connection('old_database')->table('bottles')->whereNull('deleted_at')->get();
|
||||||
|
|
||||||
|
$this->info("Found {$oldBottles->count()} bottles to migrate.");
|
||||||
|
$bar = $this->output->createProgressBar($oldBottles->count());
|
||||||
|
|
||||||
|
$migrated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ($oldBottles as $oldBottle) {
|
||||||
|
$existingBottle = Bottle::where('name', $oldBottle->name)->first();
|
||||||
|
|
||||||
|
if (! $this->option('recreate') && $existingBottle) {
|
||||||
|
$this->warn("Bottle '{$oldBottle->name}' already exists, skipping...");
|
||||||
|
$skipped++;
|
||||||
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract size from name if possible (e.g., "Botol 50ml" -> 50)
|
||||||
|
$size = $this->extractSizeFromName($oldBottle->name);
|
||||||
|
|
||||||
|
$bottle = Bottle::create([
|
||||||
|
'name' => $oldBottle->name,
|
||||||
|
'slug' => Str::slug($oldBottle->name),
|
||||||
|
'size' => $size,
|
||||||
|
'cost_price' => $oldBottle->buying_price,
|
||||||
|
'sale_price' => $oldBottle->selling_price,
|
||||||
|
'description' => null,
|
||||||
|
'views' => 0,
|
||||||
|
'created_at' => $oldBottle->created_at,
|
||||||
|
'updated_at' => $oldBottle->updated_at,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create outlet relations from bottle_stocks
|
||||||
|
$this->createBottleOutletRelations($bottle->id, $oldBottle->id);
|
||||||
|
|
||||||
|
$migrated++;
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
$this->info('✅ Bottles migration completed!');
|
||||||
|
$this->info("📊 Migrated: {$migrated} bottles");
|
||||||
|
$this->info("⏭️ Skipped: {$skipped} bottles (already exist)");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("❌ Migration failed: {$e->getMessage()}");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractSizeFromName(string $name): int
|
||||||
|
{
|
||||||
|
// Try to extract size from name (e.g., "Botol 50ml" -> 50)
|
||||||
|
preg_match('/(\d+)/', $name, $matches);
|
||||||
|
|
||||||
|
return $matches ? (int) $matches[0] : 100; // Default to 100ml if not found
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createBottleOutletRelations(int $newBottleId, int $oldBottleId): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Get bottle stocks from old database
|
||||||
|
$bottleStocks = DB::connection('old_database')
|
||||||
|
->table('bottle_stocks')
|
||||||
|
->where('bottle_id', $oldBottleId)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($bottleStocks as $stock) {
|
||||||
|
// Map old store_id to new outlet_id
|
||||||
|
$outletMapping = $this->getOutletMapping();
|
||||||
|
$outletId = $outletMapping[$stock->store_id] ?? null;
|
||||||
|
|
||||||
|
if ($outletId) {
|
||||||
|
// Create many-to-many relation with stock amount
|
||||||
|
DB::table('bottle_outlet')->insert([
|
||||||
|
'bottle_id' => $newBottleId,
|
||||||
|
'outlet_id' => $outletId,
|
||||||
|
'stock' => $stock->amount,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->warn("Could not create outlet relations for bottle ID {$newBottleId}: {$e->getMessage()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getOutletMapping(): array
|
||||||
|
{
|
||||||
|
// Map old store_id to new outlet_id
|
||||||
|
// This assumes outlets have been migrated first
|
||||||
|
$outlets = Outlet::select('id')->orderBy('id')->get();
|
||||||
|
|
||||||
|
$mapping = [];
|
||||||
|
foreach ($outlets as $index => $outlet) {
|
||||||
|
$mapping[$index + 1] = $outlet->id; // Old store_id starts from 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mapping;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Migrations;
|
||||||
|
|
||||||
|
use App\Enums\CategoryType;
|
||||||
|
use App\Models\Category;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class MigrateCategoriesFromOldData extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'migrate:categories {--force : Force migration without confirmation}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Migrate categories data from old database to new structure';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if (! $this->option('force') && ! $this->confirm('Are you sure you want to migrate categories data? This will add new categories to your database.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Starting categories migration...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$oldCategories = DB::connection('old_database')->table('categories')->whereNull('deleted_at')->get();
|
||||||
|
|
||||||
|
$this->info("Found {$oldCategories->count()} categories to migrate.");
|
||||||
|
$bar = $this->output->createProgressBar($oldCategories->count());
|
||||||
|
|
||||||
|
$migrated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$sortOrder = 1;
|
||||||
|
|
||||||
|
foreach ($oldCategories as $oldCategory) {
|
||||||
|
$existingCategory = Category::where('name', $oldCategory->name)->first();
|
||||||
|
|
||||||
|
if ($existingCategory) {
|
||||||
|
$this->warn("Category '{$oldCategory->name}' already exists, skipping...");
|
||||||
|
$skipped++;
|
||||||
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Category::create([
|
||||||
|
'name' => $oldCategory->name,
|
||||||
|
'slug' => Str::slug($oldCategory->name),
|
||||||
|
'description' => null,
|
||||||
|
'type' => CategoryType::PERFUME, // Default to perfume type
|
||||||
|
'sort_order' => $sortOrder++,
|
||||||
|
'created_at' => $oldCategory->created_at,
|
||||||
|
'updated_at' => $oldCategory->updated_at,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$migrated++;
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
$this->info('✅ Categories migration completed!');
|
||||||
|
$this->info("📊 Migrated: {$migrated} categories");
|
||||||
|
$this->info("⏭️ Skipped: {$skipped} categories (already exist)");
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("❌ Migration failed: {$e->getMessage()}");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
113
app/Console/Commands/Migrations/MigrateEmployeesFromOldData.php
Normal file
113
app/Console/Commands/Migrations/MigrateEmployeesFromOldData.php
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Migrations;
|
||||||
|
|
||||||
|
use App\Enums\EmploymentStatus;
|
||||||
|
use App\Enums\Gender;
|
||||||
|
use App\Models\Employee;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class MigrateEmployeesFromOldData extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'migrate:employees {--force : Force migration without confirmation}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Migrate employees data from old database to new structure';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if (! $this->option('force') && ! $this->confirm('Are you sure you want to migrate employees data? This will add new employees to your database.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Starting employees migration...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$oldBiographical = DB::connection('old_database')->table('biographical')->whereNull('deleted_at')->get();
|
||||||
|
|
||||||
|
$this->info("Found {$oldBiographical->count()} employees to migrate.");
|
||||||
|
$bar = $this->output->createProgressBar($oldBiographical->count());
|
||||||
|
|
||||||
|
$migrated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ($oldBiographical as $oldBio) {
|
||||||
|
$user = User::find($oldBio->user_id);
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
$this->warn("User ID {$oldBio->user_id} not found, skipping employee {$oldBio->name}...");
|
||||||
|
$skipped++;
|
||||||
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingEmployee = Employee::where('user_id', $oldBio->user_id)->first();
|
||||||
|
if ($existingEmployee) {
|
||||||
|
$this->warn("Employee for user ID {$oldBio->user_id} already exists, skipping...");
|
||||||
|
$skipped++;
|
||||||
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user data for additional info
|
||||||
|
$oldUser = DB::connection('old_database')->table('users')->where('id', $oldBio->user_id)->first();
|
||||||
|
|
||||||
|
Employee::create([
|
||||||
|
'user_id' => $oldBio->user_id,
|
||||||
|
'full_name' => $oldBio->name,
|
||||||
|
'code' => $this->generateEmployeeCode($oldBio->user_id),
|
||||||
|
'phone_number' => $oldBio->phone_number,
|
||||||
|
'base_salary' => $oldUser ? $oldUser->salary : 0,
|
||||||
|
'gender' => $oldBio->gender == '1' ? Gender::MALE : Gender::FEMALE,
|
||||||
|
'address' => $oldBio->pob, // Using place of birth as address
|
||||||
|
'birthdate' => $oldBio->dob,
|
||||||
|
'hire_date' => $oldUser ? $oldUser->entry_date : now()->toDateString(),
|
||||||
|
'resign_date' => $oldUser && $oldUser->out_date ? $oldUser->out_date : null,
|
||||||
|
'status' => EmploymentStatus::PERMANENT,
|
||||||
|
'created_at' => $oldBio->created_at,
|
||||||
|
'updated_at' => $oldBio->updated_at,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$migrated++;
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
$this->info('✅ Employees migration completed!');
|
||||||
|
$this->info("📊 Migrated: {$migrated} employees");
|
||||||
|
$this->info("⏭️ Skipped: {$skipped} employees (user not found or already exist)");
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("❌ Migration failed: {$e->getMessage()}");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateEmployeeCode(int $userId): string
|
||||||
|
{
|
||||||
|
return 'EMP'.str_pad($userId, 4, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
181
app/Console/Commands/Migrations/MigrateOutletsFromOldData.php
Normal file
181
app/Console/Commands/Migrations/MigrateOutletsFromOldData.php
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Migrations;
|
||||||
|
|
||||||
|
use App\Enums\Day;
|
||||||
|
use App\Enums\OutletStatus;
|
||||||
|
use App\Models\Facility;
|
||||||
|
use App\Models\OpeningHour;
|
||||||
|
use App\Models\Outlet;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class MigrateOutletsFromOldData extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'migrate:outlets {--force : Force migration without confirmation} {--recreate : Delete existing outlets and recreate}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Migrate outlets data from old database to new structure. Use --recreate to delete and recreate all outlets with opening hours and facilities';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if ($this->option('recreate')) {
|
||||||
|
if (! $this->confirm('Are you sure you want to DELETE ALL EXISTING OUTLETS and recreate them? This action cannot be undone.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->warn('Deleting all existing outlets...');
|
||||||
|
Outlet::query()->delete(); // This will cascade delete opening hours and facilities
|
||||||
|
$this->info('All outlets deleted.');
|
||||||
|
} elseif (! $this->option('force') && ! $this->confirm('Are you sure you want to migrate outlets data? This will add new outlets to your database.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Starting outlets migration...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$oldStores = DB::connection('old_database')->table('stores')->whereNull('deleted_at')->get();
|
||||||
|
|
||||||
|
$this->info("Found {$oldStores->count()} outlets to migrate.");
|
||||||
|
$bar = $this->output->createProgressBar($oldStores->count());
|
||||||
|
|
||||||
|
$migrated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ($oldStores as $oldStore) {
|
||||||
|
if (! $this->option('recreate')) {
|
||||||
|
$existingOutlet = Outlet::where('name', $oldStore->name)->first();
|
||||||
|
|
||||||
|
if ($existingOutlet) {
|
||||||
|
// Outlet exists, but ensure opening hours and facilities exist
|
||||||
|
$this->ensureOpeningHoursExist($existingOutlet->id);
|
||||||
|
$this->ensureFacilitiesExist($existingOutlet->id);
|
||||||
|
$this->warn("Outlet '{$oldStore->name}' already exists, ensured opening hours and facilities.");
|
||||||
|
$skipped++;
|
||||||
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$outlet = Outlet::create([
|
||||||
|
'name' => $oldStore->name,
|
||||||
|
'slug' => Str::slug($oldStore->name),
|
||||||
|
'phone_number' => '0838 1693 1711',
|
||||||
|
'address' => $oldStore->address,
|
||||||
|
'landmark' => $oldStore->benchmark,
|
||||||
|
'maps_url' => null,
|
||||||
|
'status' => $oldStore->closed ? OutletStatus::TERMINATED : OutletStatus::OPERATIONAL,
|
||||||
|
'opened_date' => $oldStore->opened,
|
||||||
|
'closed_date' => $oldStore->closed,
|
||||||
|
'created_at' => $oldStore->created_at,
|
||||||
|
'updated_at' => $oldStore->updated_at,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create opening hours for this outlet (9am-9pm)
|
||||||
|
$this->createOpeningHours($outlet->id);
|
||||||
|
|
||||||
|
// Create facilities for this outlet
|
||||||
|
$this->createFacilities($outlet->id);
|
||||||
|
|
||||||
|
$migrated++;
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
$this->info('✅ Outlets migration completed!');
|
||||||
|
$this->info("📊 Migrated: {$migrated} outlets");
|
||||||
|
$this->info("⏭️ Skipped: {$skipped} outlets (already exist)");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("❌ Migration failed: {$e->getMessage()}");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureOpeningHoursExist(int $outletId): void
|
||||||
|
{
|
||||||
|
$existingHours = OpeningHour::where('outlet_id', $outletId)->count();
|
||||||
|
|
||||||
|
if ($existingHours < 7) { // Should have 7 days
|
||||||
|
// Delete existing and recreate
|
||||||
|
OpeningHour::where('outlet_id', $outletId)->delete();
|
||||||
|
$this->createOpeningHours($outletId);
|
||||||
|
$this->info("Recreated opening hours for outlet ID: {$outletId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureFacilitiesExist(int $outletId): void
|
||||||
|
{
|
||||||
|
$existingFacilities = Facility::where('outlet_id', $outletId)->count();
|
||||||
|
|
||||||
|
if ($existingFacilities < 4) { // Should have 4 facilities
|
||||||
|
// Delete existing and recreate
|
||||||
|
Facility::where('outlet_id', $outletId)->delete();
|
||||||
|
$this->createFacilities($outletId);
|
||||||
|
$this->info("Recreated facilities for outlet ID: {$outletId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createOpeningHours(int $outletId): void
|
||||||
|
{
|
||||||
|
// Create opening hours from 9am to 9pm for all days
|
||||||
|
$daysOfWeek = [
|
||||||
|
Day::MONDAY,
|
||||||
|
Day::TUESDAY,
|
||||||
|
Day::WEDNESDAY,
|
||||||
|
Day::THURSDAY,
|
||||||
|
Day::FRIDAY,
|
||||||
|
Day::SATURDAY,
|
||||||
|
Day::SUNDAY,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($daysOfWeek as $day) {
|
||||||
|
OpeningHour::create([
|
||||||
|
'outlet_id' => $outletId,
|
||||||
|
'day' => $day,
|
||||||
|
'open_time' => '09:00:00',
|
||||||
|
'close_time' => '21:00:00',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createFacilities(int $outletId): void
|
||||||
|
{
|
||||||
|
// Facilities: parkir, sofa, air minum, permen
|
||||||
|
$facilities = [
|
||||||
|
'Parkir',
|
||||||
|
'Sofa',
|
||||||
|
'Air Minum',
|
||||||
|
'Permen',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($facilities as $facilityName) {
|
||||||
|
Facility::create([
|
||||||
|
'outlet_id' => $outletId,
|
||||||
|
'name' => $facilityName,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
166
app/Console/Commands/Migrations/MigratePerfumesFromOldData.php
Normal file
166
app/Console/Commands/Migrations/MigratePerfumesFromOldData.php
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Migrations;
|
||||||
|
|
||||||
|
use App\Enums\Concentration;
|
||||||
|
use App\Models\Outlet;
|
||||||
|
use App\Models\Perfume;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class MigratePerfumesFromOldData extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'migrate:perfumes {--force : Force migration without confirmation} {--recreate : Delete existing perfumes and recreate}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Migrate perfumes data from old database to new structure';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if ($this->option('recreate')) {
|
||||||
|
if (! $this->confirm('Are you sure you want to DELETE ALL EXISTING PERFUMES and recreate them? This action cannot be undone.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->warn('Deleting all existing perfumes...');
|
||||||
|
Perfume::query()->delete(); // This will cascade delete outlet relations
|
||||||
|
$this->info('All perfumes deleted.');
|
||||||
|
} elseif (! $this->option('force') && ! $this->confirm('Are you sure you want to migrate perfumes data? This will add new perfumes to your database.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Starting perfumes migration...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$oldFragrances = DB::connection('old_database')->table('fragrances')->whereNull('deleted_at')->get();
|
||||||
|
|
||||||
|
$this->info("Found {$oldFragrances->count()} perfumes to migrate.");
|
||||||
|
$bar = $this->output->createProgressBar($oldFragrances->count());
|
||||||
|
|
||||||
|
$migrated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ($oldFragrances as $oldFragrance) {
|
||||||
|
$existingPerfume = Perfume::where('name', $oldFragrance->name)->first();
|
||||||
|
|
||||||
|
if (! $this->option('recreate') && $existingPerfume) {
|
||||||
|
$this->warn("Perfume '{$oldFragrance->name}' already exists, skipping...");
|
||||||
|
$skipped++;
|
||||||
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$perfume = Perfume::create([
|
||||||
|
'brand_id' => $oldFragrance->brand_id,
|
||||||
|
'name' => $oldFragrance->name,
|
||||||
|
'slug' => Str::slug($oldFragrance->name),
|
||||||
|
'sku' => $this->generateSKU('PERF', $oldFragrance->id),
|
||||||
|
'concentration' => Concentration::EXTRAIT_DE_PERFUME, // Default concentration
|
||||||
|
'cost_price' => $oldFragrance->buying_price,
|
||||||
|
'sale_price' => $oldFragrance->selling_price,
|
||||||
|
'base_notes' => null,
|
||||||
|
'middle_notes' => null,
|
||||||
|
'top_notes' => null,
|
||||||
|
'description' => null,
|
||||||
|
'views' => 0,
|
||||||
|
'created_at' => $oldFragrance->created_at,
|
||||||
|
'updated_at' => $oldFragrance->updated_at,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Attach category if exists
|
||||||
|
if (isset($oldFragrance->categories_id) && $oldFragrance->categories_id) {
|
||||||
|
$perfume->categories()->attach($oldFragrance->categories_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create outlet relations from fragrance_stocks
|
||||||
|
$this->createPerfumeOutletRelations($perfume->id, $oldFragrance->id);
|
||||||
|
|
||||||
|
$migrated++;
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
$this->info('✅ Perfumes migration completed!');
|
||||||
|
$this->info("📊 Migrated: {$migrated} perfumes");
|
||||||
|
$this->info("⏭️ Skipped: {$skipped} perfumes (already exist)");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("❌ Migration failed: {$e->getMessage()}");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createPerfumeOutletRelations(int $newPerfumeId, int $oldFragranceId): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Get fragrance stocks from old database
|
||||||
|
$fragranceStocks = DB::connection('old_database')
|
||||||
|
->table('fragrance_stocks')
|
||||||
|
->where('fragrance_id', $oldFragranceId)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($fragranceStocks as $stock) {
|
||||||
|
// Map old store_id to new outlet_id
|
||||||
|
$outletMapping = $this->getOutletMapping();
|
||||||
|
$outletId = $outletMapping[$stock->store_id] ?? null;
|
||||||
|
|
||||||
|
if ($outletId) {
|
||||||
|
// Create many-to-many relation with stock amount
|
||||||
|
DB::table('outlet_perfume')->insert([
|
||||||
|
'outlet_id' => $outletId,
|
||||||
|
'perfume_id' => $newPerfumeId,
|
||||||
|
'stock' => $stock->amount,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->warn("Could not create outlet relations for perfume ID {$newPerfumeId}: {$e->getMessage()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getOutletMapping(): array
|
||||||
|
{
|
||||||
|
// Map old store_id to new outlet_id
|
||||||
|
// This assumes outlets have been migrated first
|
||||||
|
$outlets = Outlet::select('id')->orderBy('id')->get();
|
||||||
|
|
||||||
|
$mapping = [];
|
||||||
|
foreach ($outlets as $index => $outlet) {
|
||||||
|
$mapping[$index + 1] = $outlet->id; // Old store_id starts from 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateSKU(string $prefix, int $id): string
|
||||||
|
{
|
||||||
|
// Use timestamp to ensure uniqueness when recreating
|
||||||
|
$timestamp = now()->format('His');
|
||||||
|
|
||||||
|
return $prefix.str_pad($id, 3, '0', STR_PAD_LEFT).$timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
155
app/Console/Commands/Migrations/MigrateProductsFromOldData.php
Normal file
155
app/Console/Commands/Migrations/MigrateProductsFromOldData.php
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Migrations;
|
||||||
|
|
||||||
|
use App\Models\Outlet;
|
||||||
|
use App\Models\Product;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class MigrateProductsFromOldData extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'migrate:products {--force : Force migration without confirmation} {--recreate : Delete existing products and recreate}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Migrate products data from old database to new structure';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if ($this->option('recreate')) {
|
||||||
|
if (! $this->confirm('Are you sure you want to DELETE ALL EXISTING PRODUCTS and recreate them? This action cannot be undone.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->warn('Deleting all existing products...');
|
||||||
|
Product::query()->delete(); // This will cascade delete outlet relations
|
||||||
|
$this->info('All products deleted.');
|
||||||
|
} elseif (! $this->option('force') && ! $this->confirm('Are you sure you want to migrate products data? This will add new products to your database.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Starting products migration...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$oldProducts = DB::connection('old_database')->table('products')->whereNull('deleted_at')->get();
|
||||||
|
|
||||||
|
$this->info("Found {$oldProducts->count()} products to migrate.");
|
||||||
|
$bar = $this->output->createProgressBar($oldProducts->count());
|
||||||
|
|
||||||
|
$migrated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ($oldProducts as $oldProduct) {
|
||||||
|
$existingProduct = Product::where('name', $oldProduct->name)->first();
|
||||||
|
|
||||||
|
if (! $this->option('recreate') && $existingProduct) {
|
||||||
|
$this->warn("Product '{$oldProduct->name}' already exists, skipping...");
|
||||||
|
$skipped++;
|
||||||
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = Product::create([
|
||||||
|
'name' => $oldProduct->name,
|
||||||
|
'slug' => Str::slug($oldProduct->name),
|
||||||
|
'sku' => $this->generateSKU('PROD', $oldProduct->id),
|
||||||
|
'cost_price' => $oldProduct->buying_price,
|
||||||
|
'sale_price' => $oldProduct->selling_price,
|
||||||
|
'description' => null,
|
||||||
|
'views' => 0,
|
||||||
|
'created_at' => $oldProduct->created_at,
|
||||||
|
'updated_at' => $oldProduct->updated_at,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create outlet relations from product_stocks
|
||||||
|
$this->createProductOutletRelations($product->id, $oldProduct->id);
|
||||||
|
|
||||||
|
$migrated++;
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
$this->info('✅ Products migration completed!');
|
||||||
|
$this->info("📊 Migrated: {$migrated} products");
|
||||||
|
$this->info("⏭️ Skipped: {$skipped} products (already exist)");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("❌ Migration failed: {$e->getMessage()}");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createProductOutletRelations(int $newProductId, int $oldProductId): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Get product stocks from old database
|
||||||
|
$productStocks = DB::connection('old_database')
|
||||||
|
->table('product_stocks')
|
||||||
|
->where('product_id', $oldProductId)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($productStocks as $stock) {
|
||||||
|
// Map old store_id to new outlet_id
|
||||||
|
$outletMapping = $this->getOutletMapping();
|
||||||
|
$outletId = $outletMapping[$stock->store_id] ?? null;
|
||||||
|
|
||||||
|
if ($outletId) {
|
||||||
|
// Create many-to-many relation with stock amount
|
||||||
|
DB::table('outlet_product')->insert([
|
||||||
|
'outlet_id' => $outletId,
|
||||||
|
'product_id' => $newProductId,
|
||||||
|
'stock' => $stock->amount,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->warn("Could not create outlet relations for product ID {$newProductId}: {$e->getMessage()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getOutletMapping(): array
|
||||||
|
{
|
||||||
|
// Map old store_id to new outlet_id
|
||||||
|
// This assumes outlets have been migrated first
|
||||||
|
$outlets = Outlet::select('id')->orderBy('id')->get();
|
||||||
|
|
||||||
|
$mapping = [];
|
||||||
|
foreach ($outlets as $index => $outlet) {
|
||||||
|
$mapping[$index + 1] = $outlet->id; // Old store_id starts from 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateSKU(string $prefix, int $id): string
|
||||||
|
{
|
||||||
|
// Use timestamp to ensure uniqueness when recreating
|
||||||
|
$timestamp = now()->format('His');
|
||||||
|
|
||||||
|
return $prefix.str_pad($id, 3, '0', STR_PAD_LEFT).$timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
170
app/Console/Commands/Migrations/MigrateUsersFromOldData.php
Normal file
170
app/Console/Commands/Migrations/MigrateUsersFromOldData.php
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Migrations;
|
||||||
|
|
||||||
|
use App\Enums\UserStatus;
|
||||||
|
use App\Models\Outlet;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
class MigrateUsersFromOldData extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'migrate:users {--force : Force migration without confirmation}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Migrate users data from old database to new structure';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if (! $this->option('force') && ! $this->confirm('Are you sure you want to migrate users data? This will add new users to your database.')) {
|
||||||
|
$this->info('Migration cancelled.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Starting users migration...');
|
||||||
|
|
||||||
|
// Ensure roles exist
|
||||||
|
$this->ensureRolesExist();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Filter users yang tidak memiliki end_date (masih aktif)
|
||||||
|
$oldUsers = DB::connection('old_database')
|
||||||
|
->table('users')
|
||||||
|
->whereNull('out_date')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$this->info("Found {$oldUsers->count()} active users to migrate.");
|
||||||
|
$bar = $this->output->createProgressBar($oldUsers->count());
|
||||||
|
|
||||||
|
$migrated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ($oldUsers as $oldUser) {
|
||||||
|
$existingUser = User::where('email', $oldUser->email)->first();
|
||||||
|
|
||||||
|
if ($existingUser) {
|
||||||
|
$this->warn("User with email {$oldUser->email} already exists, skipping...");
|
||||||
|
$skipped++;
|
||||||
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'email' => $oldUser->email,
|
||||||
|
'username' => $this->generateUsername($oldUser->email),
|
||||||
|
'password' => Hash::make(config('myconfig.password_default')),
|
||||||
|
'status' => UserStatus::ACTIVE,
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'created_at' => $oldUser->created_at,
|
||||||
|
'updated_at' => $oldUser->updated_at,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assign role based on role_id
|
||||||
|
$roleId = $this->mapRoleId($oldUser->role_id);
|
||||||
|
if ($roleId) {
|
||||||
|
$user->assignRole($roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create outlet relation if store_id exists
|
||||||
|
if ($oldUser->store_id) {
|
||||||
|
// Map old store_id to new outlet_id
|
||||||
|
$outletMapping = $this->getOutletMapping();
|
||||||
|
$outletId = $outletMapping[$oldUser->store_id] ?? null;
|
||||||
|
|
||||||
|
if ($outletId) {
|
||||||
|
// Create many-to-many relation
|
||||||
|
DB::table('outlet_user')->insert([
|
||||||
|
'outlet_id' => $outletId,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$migrated++;
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
$this->info('✅ Users migration completed!');
|
||||||
|
$this->info("📊 Migrated: {$migrated} users");
|
||||||
|
$this->info("⏭️ Skipped: {$skipped} users (already exist)");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("❌ Migration failed: {$e->getMessage()}");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateUsername(string $email): string
|
||||||
|
{
|
||||||
|
$username = explode('@', $email)[0];
|
||||||
|
$baseUsername = $username;
|
||||||
|
$counter = 1;
|
||||||
|
|
||||||
|
while (User::where('username', $username)->exists()) {
|
||||||
|
$username = $baseUsername.$counter;
|
||||||
|
$counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mapRoleId(int $oldRoleId): ?string
|
||||||
|
{
|
||||||
|
return match ($oldRoleId) {
|
||||||
|
1 => 'Owner',
|
||||||
|
2 => 'Leader',
|
||||||
|
3 => 'Admin',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureRolesExist(): void
|
||||||
|
{
|
||||||
|
$roles = ['Owner', 'Leader', 'Admin'];
|
||||||
|
|
||||||
|
foreach ($roles as $roleName) {
|
||||||
|
Role::firstOrCreate(['name' => $roleName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Roles verified/created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getOutletMapping(): array
|
||||||
|
{
|
||||||
|
// Map old store_id to new outlet_id
|
||||||
|
// This assumes outlets have been migrated first
|
||||||
|
$outlets = Outlet::select('id')->orderBy('id')->get();
|
||||||
|
|
||||||
|
$mapping = [];
|
||||||
|
foreach ($outlets as $index => $outlet) {
|
||||||
|
$mapping[$index + 1] = $outlet->id; // Old store_id starts from 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mapping;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -63,6 +63,26 @@
|
|||||||
]) : [],
|
]) : [],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'old_database' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('SECOND_DB_URL'),
|
||||||
|
'host' => env('SECOND_DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('SECOND_DB_PORT', '3306'),
|
||||||
|
'database' => env('SECOND_DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('SECOND_DB_USERNAME', 'root'),
|
||||||
|
'password' => env('SECOND_DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('SECOND_DB_SOCKET', ''),
|
||||||
|
'charset' => env('SECOND_DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('SECOND_DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
'mariadb' => [
|
'mariadb' => [
|
||||||
'driver' => 'mariadb',
|
'driver' => 'mariadb',
|
||||||
'url' => env('DB_URL'),
|
'url' => env('DB_URL'),
|
||||||
|
|||||||
@ -14,6 +14,9 @@ public function run(): void
|
|||||||
WarehouseSeeder::class,
|
WarehouseSeeder::class,
|
||||||
TierSeeder::class,
|
TierSeeder::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->command->call('migrate:all', ['--force' => true, '--skip-confirm' => true]);
|
||||||
|
$this->command->newLine();
|
||||||
} elseif (app()->environment(['staging'])) {
|
} elseif (app()->environment(['staging'])) {
|
||||||
$this->call([
|
$this->call([
|
||||||
RolePermissionSeeder::class,
|
RolePermissionSeeder::class,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user