simedkom/app/Console/Commands/MigrateMediaOrdersCommand.php

534 lines
18 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Models\Cooperation;
use App\Models\Report;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class MigrateMediaOrdersCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrate:media-orders';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate media_orders from legacy database → cooperations, task assignments, and reports';
/**
* Cache for migrated cooperation titles to avoid redundant processing.
*/
private array $migratedTitles = [];
/**
* Cache for cooperation IDs that already have an offer file in media library.
*/
private array $migratedOfferIds = [];
/**
* Cache for migrated report IDs that already have proof media.
*/
private array $migratedReportIds = [];
/**
* Execute the console command.
*/
public function handle(): void
{
$this->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("<info>[group]</info> Processing: <comment>{$label}</comment>... ");
try {
// Skip if already migrated
if (isset($this->migratedTitles[$groupKey])) {
$this->output->writeln('<info>SKIP (already migrated)</info>');
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('<info>DONE</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
// =========================================================
// 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(
" <fg=yellow> ↳ 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(
" <fg=yellow> ↳ report #{$legacyReport->id} SKIP (media #{$partnerMediaId} tidak terdaftar di kerja sama ini — tidak ada di media_orders.media)</>"
);
return;
}
[$cleanTitle, $cleanLink] = $this->extractLinkData($legacyReport);
$exists = DB::table('reports')
->where('media_task_assignment_id', $mediaTaskAssignment->id)
->where('publication_date', $legacyReport->broadcast)
->where('link', $cleanLink)
->exists();
if ($exists) {
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();
// // 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;
$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,
};
}
}