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:
Yoga Pangestu 2026-01-10 19:04:31 +07:00
parent 6c0db0af4e
commit 3a05ee2617
10 changed files with 1223 additions and 0 deletions

View 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.');
}
}
}

View 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;
}
}

View File

@ -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;
}
}

View 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);
}
}

View 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,
]);
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@ -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'),

View File

@ -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,