From 198e936798acb0fd9c09153a5bffe0ccf6d0482e Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Tue, 21 Apr 2026 13:10:43 +0700 Subject: [PATCH] refactor: split monolithic MigrateCooperationCommand into dedicated migration commands for media orders, media cooperation, and reports --- .../Commands/MigrateCooperationCommand.php | 320 ----------- .../MigrateMediaCooperationCommand.php | 310 +++++++++++ .../Commands/MigrateMediaOrdersCommand.php | 519 ++++++++++++++++++ .../Commands/MigrateReportsCommand.php | 230 ++++++++ 4 files changed, 1059 insertions(+), 320 deletions(-) delete mode 100644 app/Console/Commands/MigrateCooperationCommand.php create mode 100644 app/Console/Commands/MigrateMediaCooperationCommand.php create mode 100644 app/Console/Commands/MigrateMediaOrdersCommand.php create mode 100644 app/Console/Commands/MigrateReportsCommand.php diff --git a/app/Console/Commands/MigrateCooperationCommand.php b/app/Console/Commands/MigrateCooperationCommand.php deleted file mode 100644 index 3abb791..0000000 --- a/app/Console/Commands/MigrateCooperationCommand.php +++ /dev/null @@ -1,320 +0,0 @@ - new cooperation_id - * Dipakai untuk lookup saat migrate proposals - */ - private array $legacyCoopMap = []; - - public function handle(): void - { - $this->migrateMediaOrders(); - $this->migrateMediaCooperation(); - $this->migrateProposals(); - $this->migrateReports(); - } - - private function migrateMediaOrders(): void - { - $this->info('Migrating media_orders → cooperations...'); - - $groups = DB::connection('mysql_second') - ->table('media_orders') - ->whereNull('deleted_at') - ->orderBy('id') - ->get() - ->groupBy(fn ($item) => strtolower(trim($item->name))); - - foreach ($groups as $group) { - $first = $group->first(); - $cooperationId = $this->insertCooperation( - name: $first->name, - description: $first->description, - initialDate: $group->min('begin'), - finalDate: $group->max('end'), - status: CooperationStatus::COMPLETED->value, - completedAt: $group->max('end'), - ); - - $this->insertCooperationMedia( - group: $group, - cooperationId: $cooperationId, - approvalStatus: ApprovalStatus::ACCEPTED->value, - ); - - $startDate = Carbon::parse($group->max('end'))->addDay(); - $endDate = $startDate->copy()->addDays(7); - - DB::table('task_assignments')->insert([ - 'cooperation_id' => $cooperationId, - 'start_date' => $startDate->toDateString(), - 'end_date' => $endDate->toDateString(), - 'task_description' => $first->description ?? '', - 'report_amount' => $group->sum('amount_report') ?: 1, - 'created_at' => now(), - 'updated_at' => now(), - ]); - - $taskAssignmentId = (int) DB::getPdo()->lastInsertId(); - - $this->insertMediaTaskAssignment( - cooperationId: $cooperationId, - taskAssignmentId: $taskAssignmentId, - approvalStatus: ApprovalStatus::ACCEPTED->value, - ); - - $this->info("Merged {$group->count()} rows → {$first->name}"); - } - - $this->info('media_orders migration done.'); - } - - private function migrateMediaCooperation(): void - { - $this->info('Migrating media_cooperation → cooperations...'); - - $groups = DB::connection('mysql_second') - ->table('media_cooperation') - ->whereNull('deleted_at') - ->orderBy('id') - ->get() - ->groupBy(fn ($item) => strtolower(trim($item->name))); - - foreach ($groups as $group) { - $first = $group->first(); - $cooperationId = $this->insertCooperation( - name: $first->name, - description: $first->description, - initialDate: $group->min('begin'), - finalDate: $group->max('end'), - status: CooperationStatus::PENDING->value, - completedAt: null, - ); - - $this->insertCooperationMedia( - group: $group, - cooperationId: $cooperationId, - approvalStatus: ApprovalStatus::PENDING->value, - ); - - $startDate = Carbon::parse($group->max('end'))->addDay(); - $endDate = $startDate->copy()->addDays(7); - - DB::table('task_assignments')->insert([ - 'cooperation_id' => $cooperationId, - 'start_date' => $startDate->toDateString(), - 'end_date' => $endDate->toDateString(), - 'task_description' => $first->description ?? '', - 'report_amount' => 1, - 'created_at' => now(), - 'updated_at' => now(), - ]); - - $taskAssignmentId = (int) DB::getPdo()->lastInsertId(); - - $this->insertMediaTaskAssignment( - cooperationId: $cooperationId, - taskAssignmentId: $taskAssignmentId, - approvalStatus: ApprovalStatus::PENDING->value, - ); - - // Simpan mapping legacy id → new cooperation_id untuk dipakai saat migrate proposals - foreach ($group as $item) { - $this->legacyCoopMap[$item->id] = $cooperationId; - } - - $this->info("Merged {$group->count()} rows → {$first->name}"); - } - - $this->info('media_cooperation migration done.'); - } - - private function migrateProposals(): void - { - $this->info('Migrating propose_media_cooperation → cooperation_proposals...'); - - $proposals = DB::connection('mysql_second') - ->table('propose_media_cooperation') - ->whereNull('deleted_at') - ->get(); - - foreach ($proposals as $proposal) { - $cooperationId = $this->legacyCoopMap[$proposal->media_cooperation_id] ?? null; - - if (! $cooperationId) { - $this->warn("cooperation not found for proposal {$proposal->id} (media_cooperation_id: {$proposal->media_cooperation_id})"); - - continue; - } - - $partnerMediaId = DB::table('partner_media') - ->join('companies', 'partner_media.company_id', '=', 'companies.id') - ->where('companies.user_id', $proposal->enhancer) - ->value('partner_media.id'); - - if (! $partnerMediaId) { - $this->warn("partner_media not found for proposal {$proposal->id} (enhancer/user_id: {$proposal->enhancer})"); - - continue; - } - - DB::table('cooperation_proposals')->insert([ - 'cooperation_id' => $cooperationId, - 'partner_media_id' => $partnerMediaId, - 'description' => null, - 'e_catalog' => null, - 'status' => $this->mapApprovalStatus($proposal->status), - 'submitted_at' => $proposal->created_at ?? now(), - 'responded_at' => $proposal->updated_at, - 'created_at' => $proposal->created_at ?? now(), - 'updated_at' => $proposal->updated_at ?? now(), - ]); - } - - $this->info('proposals migration done.'); - } - - // ========================================================= - // Helper Methods - // ========================================================= - - private function insertCooperation( - string $name, - ?string $description, - string $initialDate, - string $finalDate, - string $status, - ?string $completedAt, - ): int { - DB::table('cooperations')->insert([ - 'title' => $name, - 'initial_submission_date' => $initialDate, - 'final_submission_date' => $finalDate, - 'description' => $description ?? '', - 'payment_amount' => null, - 'payment_date' => null, - 'completed_at' => $completedAt, - 'status' => $status, - 'created_at' => now(), - 'updated_at' => now(), - ]); - - return (int) DB::getPdo()->lastInsertId(); - } - - private function insertCooperationMedia( - Collection $group, - int $cooperationId, - string $approvalStatus, - ): void { - $validMediaIds = DB::table('partner_media')->pluck('id')->toArray(); - - $mediaIds = collect(); - - foreach ($group as $item) { - if (! $item->media) { - continue; - } - $mediaIds = $mediaIds->merge(explode(',', $item->media)); - } - - $mediaIds - ->map(fn ($id) => (int) $id) - ->filter(fn ($id) => in_array($id, $validMediaIds)) - ->unique() - ->each(function ($mediaId) use ($cooperationId, $approvalStatus) { - DB::table('cooperation_media')->insert([ - 'cooperation_id' => $cooperationId, - 'partner_media_id' => $mediaId, - 'status' => $approvalStatus, - 'created_at' => now(), - 'updated_at' => now(), - ]); - }); - } - - private function mapApprovalStatus(string $legacy): string - { - return match ($legacy) { - 'accepted' => ApprovalStatus::ACCEPTED->value, - 'rejected' => ApprovalStatus::REJECTED->value, - default => ApprovalStatus::PENDING->value, - }; - } - - private function insertMediaTaskAssignment( - int $cooperationId, - int $taskAssignmentId, - string $approvalStatus, - ): void { - $mediaIds = DB::table('cooperation_media') - ->where('cooperation_id', $cooperationId) - ->pluck('partner_media_id'); - - foreach ($mediaIds as $partnerMediaId) { - DB::table('media_task_assignment')->insert([ - 'task_assignment_id' => $taskAssignmentId, - 'partner_media_id' => $partnerMediaId, - 'status' => $approvalStatus, - 'created_at' => now(), - 'updated_at' => now(), - ]); - } - } - - private function migrateReports(): void - { - $this->info('Migrating reports → reports...'); - - $legacyReports = DB::connection('mysql_second') - ->table('reports') - ->whereNull('deleted_at') - ->get(); - - foreach ($legacyReports as $legacy) { - // Cari media_task_assignment berdasarkan partner_media_id - // dan task_assignment yang berelasi ke cooperation dengan tanggal yang sesuai - $mediaTaskAssignment = DB::table('media_task_assignment') - ->join('task_assignments', 'media_task_assignment.task_assignment_id', '=', 'task_assignments.id') - ->where('media_task_assignment.partner_media_id', $legacy->media) - ->whereDate('task_assignments.start_date', '<=', $legacy->broadcast) - ->whereDate('task_assignments.end_date', '>=', $legacy->broadcast) - ->select('media_task_assignment.id') - ->first(); - - if (! $mediaTaskAssignment) { - $this->warn("media_task_assignment not found for report {$legacy->id}"); - - continue; - } - - DB::table('reports')->insert([ - 'media_task_assignment_id' => $mediaTaskAssignment->id, - 'title' => $legacy->title, - 'publication_date' => $legacy->broadcast, - 'link' => $legacy->link, - 'description' => $legacy->proof ?? '', - 'status' => $this->mapApprovalStatus($legacy->status), - 'created_at' => $legacy->created_at ?? now(), - 'updated_at' => $legacy->updated_at ?? now(), - ]); - } - - $this->info('reports migration done.'); - } -} diff --git a/app/Console/Commands/MigrateMediaCooperationCommand.php b/app/Console/Commands/MigrateMediaCooperationCommand.php new file mode 100644 index 0000000..42393b6 --- /dev/null +++ b/app/Console/Commands/MigrateMediaCooperationCommand.php @@ -0,0 +1,310 @@ +info('Starting migration: media_cooperation → cooperations...'); + + $legacyConn = DB::connection('mysql_second'); + $query = $legacyConn->table('media_cooperation')->whereNull('deleted_at'); + + try { + $totalCount = $query->count(); + } catch (\Exception $e) { + $this->error('Table media_cooperation not found or error accessing legacy database.'); + $this->error($e->getMessage()); + + return; + } + + if ($totalCount === 0) { + $this->warn('No media_cooperation found in legacy database.'); + + return; + } + + $this->info("Found {$totalCount} legacy media_cooperation records."); + + // Pre-cache migrated cooperation titles + $this->migratedTitles = DB::table('cooperations') + ->pluck('title') + ->map(fn ($t) => strtolower(trim($t))) + ->flip() + ->all(); + + // Pre-cache proposal media + $this->migratedProposalIds = DB::table('media') + ->where('model_type', CooperationProposal::class) + ->where('collection_name', 'proposals') + ->pluck('model_id') + ->flip() + ->all(); + + // Pre-load User to Partner Media mapping + $this->loadUserToMediaMap(); + + // Load all records and group by name + $all = $query->orderBy('id')->get(); + $groups = $all->groupBy(fn ($item) => strtolower(trim($item->name))); + + $this->info("Grouped into {$groups->count()} unique cooperation group(s)."); + + foreach ($groups as $groupKey => $group) { + $this->processGroup($groupKey, $group); + } + + $this->newLine(); + $this->info('media_cooperation migration completed successfully!'); + } + + /** + * Load mapping from User ID to Partner Media ID. + * Path: User -> Company -> PartnerMedia + */ + private function loadUserToMediaMap(): void + { + $this->userToMediaMap = DB::table('partner_media') + ->join('companies', 'partner_media.company_id', '=', 'companies.id') + ->whereNotNull('companies.user_id') + ->pluck('partner_media.id', 'companies.user_id') + ->all(); + } + + /** + * Process a single group of media_cooperation records. + */ + private function processGroup(string $groupKey, Collection $group): void + { + $first = $group->first(); + $label = Str::limit($first->name, 40); + $this->output->write("[group] Processing: {$label}... "); + + try { + // Check if cooperation already exists by title + $existingCooperation = DB::table('cooperations') + ->whereRaw('LOWER(TRIM(title)) = ?', [$groupKey]) + ->first(); + + if ($existingCooperation) { + $cooperationId = $existingCooperation->id; + $this->output->write('JOINING EXISTING... '); + } else { + $cooperationId = $this->insertCooperation( + name: $first->name, + description: $first->description, + initialDate: $group->min('begin'), + finalDate: $group->max('end'), + status: CooperationStatus::COMPLETED->value, + ); + $this->output->write('CREATED NEW... '); + } + + // Optional: insert cooperation_media if the legacy table has a 'media' column with IDs + if (isset($first->media)) { + $this->insertCooperationMedia($group, $cooperationId); + } + + // Migrate proposals belonging to this group's media_cooperation + $this->migrateProposals( + group: $group, + cooperationId: $cooperationId + ); + + $this->output->writeln('DONE'); + } catch (\Exception $e) { + $this->output->writeln('FAILED: '.$e->getMessage().''); + } + } + + private function insertCooperation( + string $name, + ?string $description, + ?string $initialDate, + ?string $finalDate, + string $status + ): int { + DB::table('cooperations')->insert([ + 'title' => $name, + 'initial_submission_date' => $initialDate, + 'final_submission_date' => $finalDate, + 'description' => $description ?? '', + 'status' => $status, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return (int) DB::getPdo()->lastInsertId(); + } + + private function insertCooperationMedia(Collection $group, int $cooperationId): void + { + $validMediaIds = DB::table('partner_media')->pluck('id')->toArray(); + $mediaStatusMap = []; + + foreach ($group as $item) { + if (empty($item->media)) { + continue; + } + + $ids = array_map('trim', explode(',', $item->media)); + $statuses = array_map('trim', explode(',', $item->status ?? '')); + + foreach ($ids as $index => $rawId) { + $mediaId = (int) $rawId; + if (! in_array($mediaId, $validMediaIds)) { + continue; + } + + $legacyStatus = $statuses[$index] ?? '1'; + $mediaStatusMap[$mediaId] = $this->mapApprovalStatus($legacyStatus); + } + } + + foreach ($mediaStatusMap as $mediaId => $approvalStatus) { + DB::table('cooperation_media')->insert([ + 'cooperation_id' => $cooperationId, + 'partner_media_id' => $mediaId, + 'status' => $approvalStatus, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + private function migrateProposals(Collection $group, int $cooperationId): void + { + $legacyConn = DB::connection('mysql_second'); + $legacyIds = $group->pluck('id')->toArray(); + + $legacyProposals = $legacyConn->table('propose_media_cooperation') + ->whereIn('media_cooperation_id', $legacyIds) + ->whereNull('deleted_at') + ->get(); + + foreach ($legacyProposals as $legacyProposal) { + $this->processProposal($legacyProposal, $cooperationId); + } + } + + private function processProposal(object $legacy, int $cooperationId): void + { + $userId = $legacy->enhancer; + $partnerMediaId = $this->userToMediaMap[$userId] ?? null; + + if (! $partnerMediaId) { + // Fallback or skip if media not found + return; + } + + // Check if this proposal already exists to avoid duplication + $exists = DB::table('cooperation_proposals') + ->where('cooperation_id', $cooperationId) + ->where('partner_media_id', $partnerMediaId) + ->exists(); + + if ($exists) { + return; + } + + DB::table('cooperation_proposals')->insert([ + 'cooperation_id' => $cooperationId, + 'partner_media_id' => $partnerMediaId, + 'status' => $this->mapApprovalStatus((string) $legacy->status), + 'submitted_at' => $legacy->created_at ?? now(), + 'created_at' => $legacy->created_at ?? now(), + 'updated_at' => $legacy->updated_at ?? now(), + ]); + + $proposalId = (int) DB::getPdo()->lastInsertId(); + + // Migrate offer_file if exists and not already migrated + if (! empty($legacy->offer_file) && ! isset($this->migratedProposalIds[$proposalId])) { + $this->importOfferFile($proposalId, $legacy); + } + } + + private function importOfferFile(int $proposalId, object $legacy): void + { + $legacyPath = trim($legacy->offer_file); + if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') { + return; + } + + // Adjust path based on where these files are stored in S3 + $sourcePath = 'old-data/proposals/'.$legacyPath; + + try { + if (Storage::disk('s3')->exists($sourcePath)) { + $proposal = CooperationProposal::find($proposalId); + if ($proposal) { + $proposal->addMediaFromDisk($sourcePath, 's3') + ->withCustomProperties([ + 'feature' => 'proposals', + 'date' => Carbon::parse($legacy->created_at ?? now())->toDateString(), + ]) + ->preservingOriginal() + ->toMediaCollection('proposals', 's3'); + } + } + } catch (\Exception $e) { + // Log silently + } + } + + private function mapApprovalStatus(string $legacyStatus): int + { + return match ($legacyStatus) { + '0' => ApprovalStatus::PENDING->value, + '1' => ApprovalStatus::ACCEPTED->value, + '2' => ApprovalStatus::REJECTED->value, + default => ApprovalStatus::ACCEPTED->value, + }; + } +} diff --git a/app/Console/Commands/MigrateMediaOrdersCommand.php b/app/Console/Commands/MigrateMediaOrdersCommand.php new file mode 100644 index 0000000..ff02076 --- /dev/null +++ b/app/Console/Commands/MigrateMediaOrdersCommand.php @@ -0,0 +1,519 @@ +info('Starting migration: media_orders → cooperations...'); + + $legacyConn = DB::connection('mysql_second'); + $query = $legacyConn->table('media_orders')->whereNull('deleted_at'); + + $totalCount = $query->count(); + if ($totalCount === 0) { + $this->warn('No media_orders found in legacy database.'); + + return; + } + + $this->info("Found {$totalCount} legacy media_order records."); + + // Pre-cache migrated cooperation titles to skip already processed groups + $this->migratedTitles = DB::table('cooperations') + ->pluck('title') + ->map(fn ($t) => strtolower(trim($t))) + ->flip() + ->all(); + + // Pre-cache cooperation IDs that already have an offer file + $this->migratedOfferIds = DB::table('media') + ->where('model_type', Cooperation::class) + ->where('collection_name', 'cooperations') + ->pluck('model_id') + ->flip() + ->all(); + + // Pre-cache report IDs that already have proof media (skip re-importing files) + $this->migratedReportIds = DB::table('media') + ->where('model_type', Report::class) + ->where('collection_name', 'reports') + ->pluck('model_id') + ->flip() + ->all(); + + // Load all records first (needed for groupBy), then process in chunks via collection + $all = $query->orderBy('id')->get(); + $groups = $all->groupBy(fn ($item) => strtolower(trim($item->name))); + + $this->info("Grouped into {$groups->count()} unique cooperation group(s)."); + + foreach ($groups->chunk(50) as $chunk) { + foreach ($chunk as $groupKey => $group) { + $this->processGroup($groupKey, $group); + } + } + + $this->newLine(); + $this->info('media_orders migration completed successfully!'); + } + + /** + * Process a single group of media_order records (one cooperation). + */ + private function processGroup(string $groupKey, Collection $group): void + { + $first = $group->first(); + $label = Str::limit($first->name, 40); + $this->output->write("[group] Processing: {$label}... "); + + try { + // Skip if already migrated + if (isset($this->migratedTitles[$groupKey])) { + $this->output->writeln('SKIP (already migrated)'); + + return; + } + + $startDate = Carbon::parse($group->max('end'))->addDay(); + $endDate = $startDate->copy()->addDays(7); + $paymentDate = $endDate->copy()->addDays(1); + $completedAt = $paymentDate->copy()->addDays(7); + + $cooperationId = $this->insertCooperation( + name: $first->name, + description: $first->description, + initialDate: $group->min('begin'), + finalDate: $group->max('end'), + status: CooperationStatus::COMPLETED->value, + paymentDate: $paymentDate->toDateString(), + completedAt: $completedAt->toDateString(), + ); + + $this->insertCooperationMedia( + group: $group, + cooperationId: $cooperationId, + ); + + DB::table('task_assignments')->insert([ + 'cooperation_id' => $cooperationId, + 'start_date' => $startDate->toDateString(), + 'end_date' => $endDate->toDateString(), + 'task_description' => $first->description ?? '', + 'report_amount' => $group->sum('amount_report'), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $taskAssignmentId = (int) DB::getPdo()->lastInsertId(); + + $this->insertMediaTaskAssignment( + cooperationId: $cooperationId, + taskAssignmentId: $taskAssignmentId, + approvalStatus: ApprovalStatus::ACCEPTED->value, + ); + + // // Migrate offer_file_template documents for each item in the group + // if (! isset($this->migratedOfferIds[$cooperationId])) { + // $cooperation = Cooperation::find($cooperationId); + // if ($cooperation) { + // $this->migrateOfferFiles($cooperation, $group); + // } + // } + + // Migrate reports belonging to this group's media_orders + $this->migrateReports( + group: $group, + cooperationId: $cooperationId, + taskAssignmentId: $taskAssignmentId, + ); + + // Cache so re-runs skip this title + $this->migratedTitles[$groupKey] = true; + + $this->output->writeln('DONE'); + } catch (\Exception $e) { + $this->output->writeln('FAILED: '.$e->getMessage().''); + } + } + + // ========================================================= + // Helper Methods + // ========================================================= + + private function insertCooperation( + string $name, + ?string $description, + string $initialDate, + string $finalDate, + string $status, + ?string $paymentDate, + ?string $completedAt, + ): int { + DB::table('cooperations')->insert([ + 'title' => $name, + 'initial_submission_date' => $initialDate, + 'final_submission_date' => $finalDate, + 'description' => $description ?? '', + 'payment_amount' => null, + 'payment_date' => $paymentDate, + 'completed_at' => $completedAt, + 'status' => $status, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return (int) DB::getPdo()->lastInsertId(); + } + + private function insertCooperationMedia( + Collection $group, + int $cooperationId, + ): void { + $validMediaIds = DB::table('partner_media')->pluck('id')->toArray(); + + // Build a map of mediaId → ApprovalStatus from all items in the group. + // Columns 'media' and 'status' are parallel comma-separated arrays: + // e.g. media="1,2,3" status="1,0,2" means media 1=accepted, 2=pending, 3=rejected + $mediaStatusMap = []; + + foreach ($group as $item) { + if (! $item->media) { + continue; + } + + $ids = array_map('trim', explode(',', $item->media)); + $statuses = array_map('trim', explode(',', $item->status ?? '')); + + foreach ($ids as $index => $rawId) { + $mediaId = (int) $rawId; + $legacyStatus = $statuses[$index] ?? '0'; + + if (! in_array($mediaId, $validMediaIds)) { + $this->output->writeln( + " ↳ media #{$mediaId} SKIP (tidak ditemukan di tabel partner_media, report-nya akan ikut hilang)" + ); + + continue; + } + + // Last status seen wins (latest record in group takes precedence) + $mediaStatusMap[$mediaId] = $this->mapApprovalStatus($legacyStatus); + } + } + + foreach ($mediaStatusMap as $mediaId => $approvalStatus) { + DB::table('cooperation_media')->insert([ + 'cooperation_id' => $cooperationId, + 'partner_media_id' => $mediaId, + 'status' => $approvalStatus, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + /** + * Migrate offer_file_template files for each item in the group to the Cooperation media library. + */ + private function migrateOfferFiles(Cooperation $cooperation, Collection $group): void + { + foreach ($group as $item) { + $legacyPath = trim($item->offer_file_template ?? ''); + if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') { + continue; + } + + $sourcePath = 'old-data/mediaorders/'.$legacyPath; + $this->importOfferFile($cooperation, $sourcePath, $item); + } + } + + /** + * Import a single offer file to the Cooperation media library. + */ + private function importOfferFile(Cooperation $cooperation, string $sourcePath, object $item): void + { + $attempts = 0; + $maxAttempts = 3; + $exists = false; + + while ($attempts < $maxAttempts) { + try { + $exists = Storage::disk('s3')->exists($sourcePath); + break; + } catch (\Exception $e) { + $attempts++; + usleep(200000); + } + } + + if (! $exists) { + return; + } + + try { + $cooperation->addMediaFromDisk($sourcePath, 's3') + ->withCustomProperties([ + 'feature' => 'cooperations', + 'date' => Carbon::parse($item->created_at ?? now())->toDateString(), + 'doc_type' => 'proposal-template', + ]) + ->preservingOriginal() + ->toMediaCollection('cooperations', 's3'); + } catch (\Exception $e) { + // Log quietly or handle error + } + } + + private function insertMediaTaskAssignment( + int $cooperationId, + int $taskAssignmentId, + string $approvalStatus, + ): void { + $mediaIds = DB::table('cooperation_media') + ->where('cooperation_id', $cooperationId) + ->pluck('partner_media_id'); + + foreach ($mediaIds as $partnerMediaId) { + DB::table('media_task_assignment')->insert([ + 'task_assignment_id' => $taskAssignmentId, + 'partner_media_id' => $partnerMediaId, + 'status' => $approvalStatus, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + /** + * Migrate all legacy reports belonging to the given group of media_orders. + * Each item in the group can have multiple reports via reports.media_order = item.id. + */ + private function migrateReports(Collection $group, int $cooperationId, int $taskAssignmentId): void + { + $legacyConn = DB::connection('mysql_second'); + + // Collect all legacy media_order IDs in this group + $legacyOrderIds = $group->pluck('id')->toArray(); + + $legacyReports = $legacyConn + ->table('reports') + ->whereNull('deleted_at') + ->whereIn('media_order', $legacyOrderIds) + ->orderBy('id') + ->get(); + + if ($legacyReports->isEmpty()) { + return; + } + + foreach ($legacyReports as $legacyReport) { + $this->processReport( + legacyReport: $legacyReport, + cooperationId: $cooperationId, + taskAssignmentId: $taskAssignmentId, + ); + } + } + + /** + * Process a single legacy report record. + */ + private function processReport(object $legacyReport, int $cooperationId, int $taskAssignmentId): void + { + $partnerMediaId = (int) $legacyReport->media; + + // Resolve media_task_assignment via partner_media_id = legacyReport.media + $mediaTaskAssignment = DB::table('media_task_assignment') + ->where('task_assignment_id', $taskAssignmentId) + ->where('partner_media_id', $partnerMediaId) + ->select('id') + ->first(); + + if (! $mediaTaskAssignment) { + $this->output->writeln( + " ↳ report #{$legacyReport->id} SKIP (media #{$partnerMediaId} tidak terdaftar di kerja sama ini — tidak ada di media_orders.media)" + ); + + return; + } + + [$cleanTitle, $cleanLink] = $this->extractLinkData($legacyReport); + + DB::table('reports')->insert([ + 'media_task_assignment_id' => $mediaTaskAssignment->id, + 'title' => $cleanTitle, + 'publication_date' => $legacyReport->broadcast, + 'link' => $cleanLink, + 'description' => '', + 'status' => $this->mapApprovalStatus((string) $legacyReport->status), + 'created_at' => $legacyReport->created_at ?? now(), + 'updated_at' => $legacyReport->updated_at ?? now(), + ]); + + $newReportId = (int) DB::getPdo()->lastInsertId(); + + // // Migrate proof image if not already done + // if (! isset($this->migratedReportIds[$newReportId])) { + // $report = Report::find($newReportId); + // if ($report) { + // $this->migrateProof($report, $legacyReport); + // } + // } + } + + /** + * Ekstrak title & link bersih dari legacy report. + * + * Di DB lama kolom sering terbalik: + * - reports.title → berisi URL artikel + * - reports.link → berisi teks panjang (judul + isi + URL) + * + * Logika: + * 1. Jika title terdeteksi sebagai URL → title = baris pertama link (judul artikel) + * link = URL dari title + * 2. Jika tidak → ekstrak URL dari teks link, title tetap apa adanya (max 255 char) + * + * @return array{0: string, 1: string} [title, link] + */ + private function extractLinkData(object $legacyReport): array + { + $rawTitle = trim($legacyReport->title ?? ''); + $rawLink = trim($legacyReport->link ?? ''); + + $titleIsUrl = (bool) preg_match('#^https?://#i', $rawTitle); + + // Cari URL pertama di dalam teks rawLink + preg_match('#https?://\S+#', $rawLink, $urlMatches); + $extractedUrl = rtrim($urlMatches[0] ?? '', '.,;)'); + + if ($titleIsUrl) { + // title berisi URL → pakai sebagai link + $cleanLink = $rawTitle; + + // Ambil judul dari baris pertama rawLink yang bukan URL + $lines = array_values(array_filter( + array_map('trim', preg_split('/[\r\n]+/', $rawLink)) + )); + $candidateTitle = ''; + foreach ($lines as $line) { + if (! preg_match('#^https?://#i', $line)) { + $candidateTitle = $line; + break; + } + } + $cleanTitle = $candidateTitle !== '' + ? mb_substr($candidateTitle, 0, 255) + : mb_substr($rawTitle, 0, 255); + } else { + // Normal: title sudah benar; ekstrak URL bersih dari link + $cleanLink = $extractedUrl ?: mb_substr($rawLink, 0, 2048); + $cleanTitle = mb_substr($rawTitle, 0, 255); + } + + return [$cleanTitle, $cleanLink]; + } + + /** + * Migrate the proof image file from S3 to the Report media library. + */ + private function migrateProof(Report $report, object $legacy): void + { + $legacyPath = trim($legacy->proof ?? ''); + if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') { + return; + } + + $sourcePath = 'old-data/reports/'.$legacyPath; + + $attempts = 0; + $maxAttempts = 3; + $exists = false; + + while ($attempts < $maxAttempts) { + try { + $exists = Storage::disk('s3')->exists($sourcePath); + break; + } catch (\Exception $e) { + $attempts++; + usleep(200000); + } + } + + if (! $exists) { + return; + } + + try { + $report->addMediaFromDisk($sourcePath, 's3') + ->withCustomProperties([ + 'feature' => 'reports', + 'date' => Carbon::parse($legacy->created_at ?? now())->toDateString(), + 'doc_type' => 'proof', + ]) + ->preservingOriginal() + ->toMediaCollection('reports', 's3'); + } catch (\Exception $e) { + // Log quietly or handle error + } + } + + /** + * Map legacy status integer to ApprovalStatus enum value. + * Legacy: 0 = menunggu, 1 = menerima, 2 = menolak + */ + private function mapApprovalStatus(string $legacyStatus): int + { + return match ($legacyStatus) { + '0' => ApprovalStatus::PENDING->value, + '1' => ApprovalStatus::ACCEPTED->value, + '2' => ApprovalStatus::REJECTED->value, + default => ApprovalStatus::ACCEPTED->value, + }; + } +} diff --git a/app/Console/Commands/MigrateReportsCommand.php b/app/Console/Commands/MigrateReportsCommand.php new file mode 100644 index 0000000..63e8be4 --- /dev/null +++ b/app/Console/Commands/MigrateReportsCommand.php @@ -0,0 +1,230 @@ +info('Starting migration: reports → reports...'); + + $legacyConn = DB::connection('mysql_second'); + $query = $legacyConn->table('reports')->whereNull('deleted_at'); + + $totalCount = $query->count(); + if ($totalCount === 0) { + $this->warn('No reports found in legacy database.'); + + return; + } + + $this->info("Found {$totalCount} legacy report record(s)."); + + // Pre-cache report IDs that already have proof media (skip re-importing files) + $this->migratedIds = DB::table('media') + ->where('model_type', Report::class) + ->where('collection_name', 'reports') + ->pluck('model_id') + ->flip() + ->all(); + + // Build a lookup: legacy media_orders.id → new cooperations.id + // via: media_orders.name (lowercased) → cooperations.title (lowercased) + $legacyOrders = $legacyConn->table('media_orders') + ->whereNull('deleted_at') + ->select('id', 'name') + ->get() + ->keyBy('id'); + + $cooperationMap = DB::table('cooperations') + ->select('id', 'title') + ->get() + ->keyBy(fn ($c) => strtolower(trim($c->title))); + + foreach ($legacyOrders as $order) { + $key = strtolower(trim($order->name)); + if (isset($cooperationMap[$key])) { + $this->mediaOrderToCooperation[$order->id] = $cooperationMap[$key]->id; + } + } + + $query->orderBy('id')->chunk(50, function ($chunk) { + foreach ($chunk as $legacy) { + $this->processReport($legacy); + } + }); + + $this->newLine(); + $this->info('reports migration completed successfully!'); + } + + /** + * Process a single legacy report record. + */ + private function processReport(object $legacy): void + { + $idText = "[#{$legacy->id}]"; + $this->output->write("{$idText} Processing: ".Str::limit($legacy->title ?? '', 40).'... '); + + try { + // Resolve media_task_assignment_id + // Chain: legacy.media_order → cooperation_id → task_assignment → media_task_assignment (by partner_media_id) + $cooperationId = $this->mediaOrderToCooperation[$legacy->media_order] ?? null; + + if (! $cooperationId) { + $this->output->writeln('SKIP (cooperation not found for media_order #'.$legacy->media_order.')'); + + return; + } + + $taskAssignment = DB::table('task_assignments') + ->where('cooperation_id', $cooperationId) + ->select('id') + ->first(); + + if (! $taskAssignment) { + $this->output->writeln('SKIP (task_assignment not found for cooperation #'.$cooperationId.')'); + + return; + } + + $mediaTaskAssignment = DB::table('media_task_assignment') + ->where('task_assignment_id', $taskAssignment->id) + ->where('partner_media_id', $legacy->media) + ->select('id') + ->first(); + + if (! $mediaTaskAssignment) { + $this->output->writeln('SKIP (media_task_assignment not found for media #'.$legacy->media.')'); + + return; + } + + // Insert the report + DB::table('reports')->insert([ + 'media_task_assignment_id' => $mediaTaskAssignment->id, + 'title' => $legacy->title ?? '', + 'publication_date' => $legacy->broadcast, + 'link' => $legacy->link ?? '', + 'description' => '', + 'status' => $this->mapApprovalStatus((string) $legacy->status), + 'created_at' => $legacy->created_at ?? now(), + 'updated_at' => $legacy->updated_at ?? now(), + ]); + + $newReportId = (int) DB::getPdo()->lastInsertId(); + + // Migrate proof image if not already done + if (! isset($this->migratedIds[$newReportId])) { + $report = Report::find($newReportId); + if ($report) { + $this->migrateProof($report, $legacy); + } + } + + $this->output->writeln('DONE'); + } catch (\Exception $e) { + $this->output->writeln('FAILED: '.$e->getMessage().''); + } + } + + /** + * Migrate the proof image file from S3 to the Report media library. + */ + private function migrateProof(Report $report, object $legacy): void + { + $legacyPath = trim($legacy->proof ?? ''); + if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') { + return; + } + + $sourcePath = 'old-data/reports/'.$legacyPath; + $this->importProofFile($report, $sourcePath, $legacy); + } + + /** + * Import a single proof image to the Report media library. + */ + private function importProofFile(Report $report, string $sourcePath, object $legacy): void + { + $attempts = 0; + $maxAttempts = 3; + $exists = false; + + while ($attempts < $maxAttempts) { + try { + $exists = Storage::disk('s3')->exists($sourcePath); + break; + } catch (\Exception $e) { + $attempts++; + usleep(200000); + } + } + + if (! $exists) { + return; + } + + try { + $report->addMediaFromDisk($sourcePath, 's3') + ->withCustomProperties([ + 'feature' => 'reports', + 'date' => Carbon::parse($legacy->created_at ?? now())->toDateString(), + 'doc_type' => 'proof', + ]) + ->preservingOriginal() + ->toMediaCollection('reports', 's3'); + } catch (\Exception $e) { + // Log quietly or handle error + } + } + + /** + * Map legacy reports.status integer to ApprovalStatus enum value. + * Legacy: 0 = menunggu, 1 = menerima, 2 = menolak + */ + private function mapApprovalStatus(string $legacyStatus): int + { + return match ($legacyStatus) { + '0' => ApprovalStatus::PENDING->value, + '1' => ApprovalStatus::ACCEPTED->value, + '2' => ApprovalStatus::REJECTED->value, + default => ApprovalStatus::PENDING->value, + }; + } +}