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 { $fileName = basename($sourcePath); $legacyDir = dirname($sourcePath); DB::table('media')->insert([ 'model_type' => Cooperation::class, 'model_id' => $cooperation->id, 'uuid' => (string) Str::uuid(), 'collection_name' => 'cooperations', 'name' => pathinfo($fileName, PATHINFO_FILENAME), 'file_name' => $fileName, 'mime_type' => $this->guessMime($fileName), 'disk' => 's3', 'conversions_disk' => 's3', 'size' => 0, 'manipulations' => '[]', 'custom_properties' => json_encode([ 'feature' => 'cooperations', 'date' => Carbon::parse($item->created_at ?? now())->toDateString(), 'doc_type' => 'proposal-template', 'legacy_dir' => $legacyDir, ]), 'generated_conversions' => '[]', 'responsive_images' => '[]', 'order_column' => 1, 'created_at' => now(), 'updated_at' => now(), ]); } 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); $existingReportId = DB::table('reports') ->where('media_task_assignment_id', $mediaTaskAssignment->id) ->where('publication_date', $legacyReport->broadcast) ->where('link', $cleanLink) ->value('id'); if ($existingReportId) { if (! isset($this->migratedReportIds[$existingReportId])) { $report = Report::find($existingReportId); if ($report) { $this->migrateProof($report, $legacyReport); $this->migratedReportIds[$existingReportId] = true; } } return; } 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(); if (! isset($this->migratedReportIds[$newReportId])) { $report = Report::find($newReportId); if ($report) { $this->migrateProof($report, $legacyReport); $this->migratedReportIds[$newReportId] = true; } } } /** * 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; $fileName = basename($sourcePath); $legacyDir = dirname($sourcePath); DB::table('media')->insert([ 'model_type' => Report::class, 'model_id' => $report->id, 'uuid' => (string) Str::uuid(), 'collection_name' => 'reports', 'name' => pathinfo($fileName, PATHINFO_FILENAME), 'file_name' => $fileName, 'mime_type' => $this->guessMime($fileName), 'disk' => 's3', 'conversions_disk' => 's3', 'size' => 0, 'manipulations' => '[]', 'custom_properties' => json_encode([ 'feature' => 'reports', 'date' => Carbon::parse($legacy->created_at ?? now())->toDateString(), 'doc_type' => 'proof', 'legacy_dir' => $legacyDir, ]), 'generated_conversions' => '[]', 'responsive_images' => '[]', 'order_column' => 1, 'created_at' => now(), 'updated_at' => now(), ]); } private function guessMime(string $fileName): string { return match (strtolower(pathinfo($fileName, PATHINFO_EXTENSION))) { 'pdf' => 'application/pdf', 'jpg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', default => 'application/octet-stream', }; } /** * 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, }; } }