From c063cde96782bcc8b60ecbf6d9ddc0c00a8fed1c Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Tue, 4 Aug 2026 10:41:01 +0700 Subject: [PATCH] feat: implement old data migration command and related migration classes --- .../Commands/MigrateOldDataCommand.php | 115 ++++++++++++++++++ .../BaseOldDataMigration.php | 101 +++++++++++++++ .../CashFinanceMigration.php | 22 ++++ app/OldDataMigrations/CoreAuthMigration.php | 27 ++++ app/OldDataMigrations/CuttingMigration.php | 29 +++++ app/OldDataMigrations/HrMigration.php | 26 ++++ app/OldDataMigrations/InventoryMigration.php | 34 ++++++ app/OldDataMigrations/MasterDataMigration.php | 22 ++++ app/OldDataMigrations/MediaMigration.php | 25 ++++ app/OldDataMigrations/OrdersMigration.php | 40 ++++++ app/OldDataMigrations/ProductsMigration.php | 36 ++++++ app/OldDataMigrations/PurchasesMigration.php | 21 ++++ .../RawMaterialsMigration.php | 28 +++++ .../SystemConfigMigration.php | 32 +++++ 14 files changed, 558 insertions(+) create mode 100644 app/Console/Commands/MigrateOldDataCommand.php create mode 100644 app/OldDataMigrations/BaseOldDataMigration.php create mode 100644 app/OldDataMigrations/CashFinanceMigration.php create mode 100644 app/OldDataMigrations/CoreAuthMigration.php create mode 100644 app/OldDataMigrations/CuttingMigration.php create mode 100644 app/OldDataMigrations/HrMigration.php create mode 100644 app/OldDataMigrations/InventoryMigration.php create mode 100644 app/OldDataMigrations/MasterDataMigration.php create mode 100644 app/OldDataMigrations/MediaMigration.php create mode 100644 app/OldDataMigrations/OrdersMigration.php create mode 100644 app/OldDataMigrations/ProductsMigration.php create mode 100644 app/OldDataMigrations/PurchasesMigration.php create mode 100644 app/OldDataMigrations/RawMaterialsMigration.php create mode 100644 app/OldDataMigrations/SystemConfigMigration.php diff --git a/app/Console/Commands/MigrateOldDataCommand.php b/app/Console/Commands/MigrateOldDataCommand.php new file mode 100644 index 0000000..7aecce9 --- /dev/null +++ b/app/Console/Commands/MigrateOldDataCommand.php @@ -0,0 +1,115 @@ +glob($migrationPath.'/*.php')) + ->map(fn ($file) => basename($file)) + ->filter(fn ($file) => $file !== 'BaseOldDataMigration.php') + ->toArray(); + + $migrations = []; + + foreach ($files as $file) { + require_once $migrationPath.'/'.$file; + + $className = 'App\\OldDataMigrations\\'.str_replace('.php', '', $file); + + if (! class_exists($className)) { + continue; + } + + $instance = new $className; + + if ($instance instanceof BaseOldDataMigration) { + $migrations[] = $instance; + } + } + + usort($migrations, fn ($a, $b) => $a->priority() <=> $b->priority()); + + $only = $this->option('only') ? explode(',', $this->option('only')) : null; + $skip = $this->option('skip') ? explode(',', $this->option('skip')) : []; + BaseOldDataMigration::$truncateBeforeInsert = $this->option('force'); + + $this->newLine(); + $this->info('=== Migrate Old Data ==='); + $this->newLine(); + + DB::connection('mysql')->getPdo()->exec('SET FOREIGN_KEY_CHECKS=0'); + $this->warn('Foreign key checks disabled.'); + + app(PermissionRegistrar::class)->forgetCachedPermissions(); + + $totalRows = 0; + $tableSummary = []; + + foreach ($migrations as $migration) { + if ($only !== null) { + $results = $migration->migrate(); + $groupTableNames = array_keys($results); + $hasOverlap = false; + foreach ($only as $t) { + if (in_array($t, $groupTableNames)) { + $hasOverlap = true; + break; + } + } + if (! $hasOverlap) { + continue; + } + } + + $className = (new \ReflectionClass($migration))->getShortName(); + $this->info("Running: {$className}"); + + $results = $migration->migrate(); + + foreach ($results as $table => $count) { + if (in_array($table, $skip)) { + $this->line(" SKIP {$table}"); + + continue; + } + + $tableSummary[$table] = $count; + $totalRows += $count; + $this->line(" OK {$table}: {$count} rows"); + } + + $this->newLine(); + } + + DB::connection('mysql')->getPdo()->exec('SET FOREIGN_KEY_CHECKS=1'); + app(PermissionRegistrar::class)->forgetCachedPermissions(); + $this->warn('Foreign key checks re-enabled.'); + + $this->newLine(); + $this->info('=== Summary ==='); + $this->info('Total tables: '.count($tableSummary)); + $this->info("Total rows: {$totalRows}"); + $this->newLine(); + + $this->table(['Table', 'Rows'], collect($tableSummary)->map(fn ($count, $table) => [$table, $count])->values()->toArray()); + + return Command::SUCCESS; + } +} diff --git a/app/OldDataMigrations/BaseOldDataMigration.php b/app/OldDataMigrations/BaseOldDataMigration.php new file mode 100644 index 0000000..785d26d --- /dev/null +++ b/app/OldDataMigrations/BaseOldDataMigration.php @@ -0,0 +1,101 @@ +old = DB::connection('mysql_old'); + $this->new = DB::connection('mysql'); + } + + abstract public function migrate(): array; + + abstract public function priority(): int; + + protected function oldQuery(string $table) + { + return $this->old->table($table); + } + + protected function truncateIfNeeded(string $table): void + { + if (static::$truncateBeforeInsert) { + $this->new->table($table)->truncate(); + } + } + + protected function insertBatch(string $table, array $rows, int $chunkSize = 500): int + { + if (empty($rows)) { + return 0; + } + + $total = 0; + + foreach (array_chunk($rows, $chunkSize) as $chunk) { + $this->new->table($table)->insert($chunk); + $total += count($chunk); + } + + return $total; + } + + protected function migrateTable(string $table, ?callable $transform = null, ?array $columns = null): int + { + $this->truncateIfNeeded($table); + + $rows = $this->oldQuery($table)->get(); + + if ($rows->isEmpty()) { + return 0; + } + + $rows = $rows->map(function ($row) use ($columns) { + $data = (array) $row; + if ($columns !== null) { + return array_intersect_key($data, array_flip($columns)); + } + + return $data; + })->toArray(); + + if ($transform !== null) { + $rows = array_map($transform, $rows); + } + + return $this->insertBatch($table, $rows); + } + + protected function migrateTableWithoutColumns(string $table, array $skipColumns, ?callable $transform = null): int + { + $this->truncateIfNeeded($table); + + $rows = $this->oldQuery($table)->get(); + + if ($rows->isEmpty()) { + return 0; + } + + $rows = $rows->map(function ($row) use ($skipColumns) { + $data = (array) $row; + + return array_diff_key($data, array_flip($skipColumns)); + })->toArray(); + + if ($transform !== null) { + $rows = array_map($transform, $rows); + } + + return $this->insertBatch($table, $rows); + } +} diff --git a/app/OldDataMigrations/CashFinanceMigration.php b/app/OldDataMigrations/CashFinanceMigration.php new file mode 100644 index 0000000..cf645ea --- /dev/null +++ b/app/OldDataMigrations/CashFinanceMigration.php @@ -0,0 +1,22 @@ +migrateTable('cash_accounts'); + $results['cash_transactions'] = $this->migrateTable('cash_transactions'); + $results['expenses'] = $this->migrateTable('expenses'); + + return $results; + } +} diff --git a/app/OldDataMigrations/CoreAuthMigration.php b/app/OldDataMigrations/CoreAuthMigration.php new file mode 100644 index 0000000..7b6cd13 --- /dev/null +++ b/app/OldDataMigrations/CoreAuthMigration.php @@ -0,0 +1,27 @@ +migrateTable('users'); + $results['user_profiles'] = $this->migrateTable('user_profiles'); + $results['employees'] = $this->migrateTable('employees'); + $results['permissions'] = $this->migrateTable('permissions'); + $results['roles'] = $this->migrateTable('roles'); + $results['model_has_permissions'] = $this->migrateTable('model_has_permissions'); + $results['model_has_roles'] = $this->migrateTable('model_has_roles'); + $results['role_has_permissions'] = $this->migrateTable('role_has_permissions'); + + return $results; + } +} diff --git a/app/OldDataMigrations/CuttingMigration.php b/app/OldDataMigrations/CuttingMigration.php new file mode 100644 index 0000000..de56366 --- /dev/null +++ b/app/OldDataMigrations/CuttingMigration.php @@ -0,0 +1,29 @@ +migrateTable('cuttings'); + $results['cutting_material_combinations'] = $this->migrateTable('cutting_material_combinations'); + $results['cutting_materials'] = $this->migrateTable('cutting_materials', function ($row) { + if (isset($row['material_usage']) && is_string($row['material_usage'])) { + $row['material_usage'] = (int) round($row['material_usage']); + } + + return $row; + }); + $results['cutting_results'] = $this->migrateTable('cutting_results'); + + return $results; + } +} diff --git a/app/OldDataMigrations/HrMigration.php b/app/OldDataMigrations/HrMigration.php new file mode 100644 index 0000000..b498dc1 --- /dev/null +++ b/app/OldDataMigrations/HrMigration.php @@ -0,0 +1,26 @@ +migrateTable('employee_advances'); + $results['employee_advance_payments'] = $this->migrateTable('employee_advance_payments'); + $results['attendances'] = $this->migrateTable('attendances'); + $results['leave_requests'] = $this->migrateTable('leave_requests'); + $results['payroll_periods'] = $this->migrateTable('payroll_periods'); + $results['payrolls'] = $this->migrateTable('payrolls'); + $results['payroll_adjustments'] = $this->migrateTable('payroll_adjustments'); + + return $results; + } +} diff --git a/app/OldDataMigrations/InventoryMigration.php b/app/OldDataMigrations/InventoryMigration.php new file mode 100644 index 0000000..d6a2de7 --- /dev/null +++ b/app/OldDataMigrations/InventoryMigration.php @@ -0,0 +1,34 @@ +migrateTable('retail_stock_histories'); + $results['stok_opnames'] = $this->migrateTable('stok_opnames'); + $results['stok_opname_items'] = $this->migrateTable('stok_opname_items'); + $results['restocks'] = $this->migrateTable('restocks'); + $results['restock_items'] = $this->migrateTable('restock_items'); + $results['stock_mutations'] = $this->migrateTable('stock_mutations', function ($row) { + $fields = ['quantity', 'stock_before', 'stock_after']; + foreach ($fields as $field) { + if (isset($row[$field]) && is_string($row[$field])) { + $row[$field] = (int) round($row[$field]); + } + } + + return $row; + }); + + return $results; + } +} diff --git a/app/OldDataMigrations/MasterDataMigration.php b/app/OldDataMigrations/MasterDataMigration.php new file mode 100644 index 0000000..849bb68 --- /dev/null +++ b/app/OldDataMigrations/MasterDataMigration.php @@ -0,0 +1,22 @@ +migrateTable('categories'); + $results['suppliers'] = $this->migrateTable('suppliers'); + $results['customers'] = $this->migrateTable('customers'); + + return $results; + } +} diff --git a/app/OldDataMigrations/MediaMigration.php b/app/OldDataMigrations/MediaMigration.php new file mode 100644 index 0000000..4593057 --- /dev/null +++ b/app/OldDataMigrations/MediaMigration.php @@ -0,0 +1,25 @@ +migrateTable('media', function ($row) { + $row['disk'] = 's3'; + $row['conversions_disk'] = 's3'; + + return $row; + }); + + return $results; + } +} diff --git a/app/OldDataMigrations/OrdersMigration.php b/app/OldDataMigrations/OrdersMigration.php new file mode 100644 index 0000000..ce24fe3 --- /dev/null +++ b/app/OldDataMigrations/OrdersMigration.php @@ -0,0 +1,40 @@ +migrateTable('orders', function ($row) { + $row['cogs'] = $row['cogs'] ?? 0; + + $row['price_type'] = match ($row['price_type']) { + 'grosir' => 'wholesale', + 'harga_modal' => 'capital', + default => $row['price_type'], + }; + + return $row; + }); + + $results['order_items'] = $this->migrateTable('order_items', function ($row) { + $row['stock_quality'] = match ($row['stock_quality']) { + 'retail' => 'good', + default => $row['stock_quality'], + }; + + return $row; + }); + $results['activity_log'] = $this->migrateTable('activity_log'); + + return $results; + } +} diff --git a/app/OldDataMigrations/ProductsMigration.php b/app/OldDataMigrations/ProductsMigration.php new file mode 100644 index 0000000..671e25c --- /dev/null +++ b/app/OldDataMigrations/ProductsMigration.php @@ -0,0 +1,36 @@ +migrateTable('products'); + $results['product_categories'] = $this->migrateTable('product_categories'); + $results['product_variants'] = $this->migrateTable('product_variants'); + + $results['product_prices'] = $this->migrateTableWithoutColumns( + 'product_prices', + ['deleted_at'], + function ($row) { + $row['type'] = match ($row['type']) { + 'grosir' => 'wholesale', + 'harga_modal' => 'capital', + default => $row['type'], + }; + + return $row; + }, + ); + + return $results; + } +} diff --git a/app/OldDataMigrations/PurchasesMigration.php b/app/OldDataMigrations/PurchasesMigration.php new file mode 100644 index 0000000..51e9368 --- /dev/null +++ b/app/OldDataMigrations/PurchasesMigration.php @@ -0,0 +1,21 @@ +migrateTable('purchases'); + $results['purchase_items'] = $this->migrateTable('purchase_items'); + + return $results; + } +} diff --git a/app/OldDataMigrations/RawMaterialsMigration.php b/app/OldDataMigrations/RawMaterialsMigration.php new file mode 100644 index 0000000..fabd409 --- /dev/null +++ b/app/OldDataMigrations/RawMaterialsMigration.php @@ -0,0 +1,28 @@ +migrateTable('raw_materials', function ($row) { + $row['unit'] = match ($row['unit']) { + 'kilogram' => 'kg', + default => $row['unit'], + }; + + return $row; + }); + $results['raw_material_prices'] = $this->migrateTable('raw_material_prices'); + + return $results; + } +} diff --git a/app/OldDataMigrations/SystemConfigMigration.php b/app/OldDataMigrations/SystemConfigMigration.php new file mode 100644 index 0000000..7affcd3 --- /dev/null +++ b/app/OldDataMigrations/SystemConfigMigration.php @@ -0,0 +1,32 @@ +migrateTable('system_configurations'); + $results['push_subscriptions'] = $this->migrateTable('push_subscriptions', function ($row) { + if (isset($row['user_id']) && $row['user_id'] !== null) { + $row['subscribable_type'] = 'App\\Models\\User'; + $row['subscribable_id'] = $row['user_id']; + } + unset($row['user_id']); + + return $row; + }); + $results['homepage_configurations'] = $this->migrateTable('homepage_configurations'); + $results['owner_verification_requests'] = $this->migrateTable('owner_verification_requests'); + $results['notifications'] = $this->migrateTable('notifications'); + + return $results; + } +}