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