option('connection');
$batchSize = max(1, (int) $this->option('batch-size'));
$statusId = $this->option('status') !== null ? (int) $this->option('status') : null;
$resetProgress = (bool) $this->option('reset-progress');
$dryRun = (bool) $this->option('dry-run');
$sync = (bool) $this->option('sync');
// ── Pre-flight: make sure the connection and source tables exist ─────────
try {
DB::connection($connection)->getPdo();
} catch (\Throwable $e) {
$this->error("Cannot connect to Asteria DB (connection='{$connection}'): ".$e->getMessage());
$this->warn('Add an "asteria" entry to config/database.php (see the comment at the end of that file).');
return self::FAILURE;
}
if (! $this->ensureMigrationColumnExists()) {
return self::FAILURE;
}
// ── Build product and customer lookup maps ────────────────────────────────
$this->info('Building product SKU map…');
$productIdMap = $this->buildProductIdMap($connection);
$this->line(' '.count($productIdMap).' Magento products matched to Bagisto products by SKU.');
if (empty($productIdMap)) {
$this->warn('No matching products found. Are the SKUs consistent between the two stores?');
}
$this->info('Building customer e-mail map…');
$customerEmailMap = $this->buildCustomerEmailMap($connection);
$this->line(' '.count($customerEmailMap).' Magento customers matched to Bagisto customers by e-mail.');
// ── Determine starting review_id ──────────────────────────────────────────
$lastId = $resetProgress ? 0 : (int) Cache::get(self::PROGRESS_KEY, 0);
if ($lastId > 0) {
$this->line("Resuming from Asteria review_id > {$lastId} (use --reset-progress to restart).");
}
// ── Dispatch batches ──────────────────────────────────────────────────────
$totalDispatched = 0;
$totalSkipped = 0;
$batchNumber = 0;
$this->info($dryRun ? '[DRY RUN] Scanning reviews…' : 'Dispatching migration jobs…');
do {
$rows = $this->fetchBatch($connection, $lastId, $batchSize, $statusId);
if ($rows->isEmpty()) {
break;
}
$batchNumber++;
$batchRows = $rows->toArray();
// Collect unique Magento product IDs in this batch, filter unknown ones.
$unknownProducts = array_filter(
array_unique(array_column($batchRows, 'magento_product_id')),
fn ($id) => ! isset($productIdMap[$id])
);
if (! empty($unknownProducts)) {
$this->warn(" Batch #{$batchNumber}: ".count($unknownProducts).
' Magento product(s) not found in Bagisto: '.implode(', ', array_slice($unknownProducts, 0, 10)));
$totalSkipped += count(array_filter($batchRows, fn ($r) => ! isset($productIdMap[$r->magento_product_id])));
}
$lastId = max(array_column($batchRows, 'review_id'));
if (! $dryRun) {
if ($sync) {
(new MigrateReviewJob(
array_map(fn ($r) => (array) $r, $batchRows),
$productIdMap,
$customerEmailMap,
))->handle();
} else {
MigrateReviewJob::dispatch(
array_map(fn ($r) => (array) $r, $batchRows),
$productIdMap,
$customerEmailMap,
)->onQueue(self::QUEUE_NAME);
}
Cache::put(self::PROGRESS_KEY, $lastId, now()->addDays(30));
}
$totalDispatched += count($batchRows);
$this->line(sprintf(
' Batch #%d: %d reviews (last review_id=%d)%s',
$batchNumber,
count($batchRows),
$lastId,
$dryRun ? ' [skipped – dry-run]' : ($sync ? ' [processed]' : ' [enqueued]'),
));
Log::info('MigrateAsteriaReviews: batch '.$batchNumber.', '.count($batchRows).' rows, last_id='.$lastId);
} while ($rows->count() === $batchSize);
// ── Summary ───────────────────────────────────────────────────────────────
$this->newLine();
$this->info("Done. Batches: {$batchNumber}, reviews processed/enqueued: {$totalDispatched}, unmatched: {$totalSkipped}.");
if (! $dryRun && ! $sync) {
$this->line('Run php artisan queue:work --queue=review-migration to process the queue.');
}
return self::SUCCESS;
}
// ─────────────────────────────────────────────────────────────────────────────
// Private helpers
// ─────────────────────────────────────────────────────────────────────────────
/**
* Fetch one page of Magento reviews, enriched with rating and customer e-mail.
*
* Magento table names use the default prefix (none). If your Magento
* installation uses a prefix (e.g. `mag_`), set ASTERIA_DB_PREFIX in .env and
* pass it via the `prefix` key in the database connection config.
*
* @return \Illuminate\Support\Collection
*/
private function fetchBatch(
string $connection,
int $lastId,
int $limit,
?int $statusId
): \Illuminate\Support\Collection {
$query = DB::connection($connection)
->table('review AS r')
->join('review_detail AS rd', 'rd.review_id', '=', 'r.review_id')
->leftJoin('customer_entity AS ce', 'ce.entity_id', '=', 'rd.customer_id')
->leftJoinSub(
// Average all rating dimensions for each review (Quality, Value, Price…)
DB::connection($connection)
->table('rating_option_vote')
->select('review_id', DB::raw('AVG(`value`) AS avg_rating'))
->groupBy('review_id'),
'rv',
'rv.review_id',
'=',
'r.review_id'
)
->select([
'r.review_id',
'r.created_at',
'r.entity_pk_value AS magento_product_id',
'r.status_id',
'rd.title',
'rd.detail AS comment',
'rd.nickname AS name',
'rd.customer_id AS magento_customer_id',
'ce.email AS customer_email',
'rv.avg_rating',
])
->where('r.entity_id', 1) // entity_id=1 means "product" in Magento
->where('r.review_id', '>', $lastId)
->orderBy('r.review_id')
->limit($limit);
if ($statusId !== null) {
$query->where('r.status_id', $statusId);
}
$reviews = $query->get();
if ($reviews->isEmpty()) {
return $reviews;
}
// Attach image URLs from Senje_Review's review_media_image table (status_id=1 = approved).
$reviewIds = $reviews->pluck('review_id')->toArray();
$imageRows = DB::connection($connection)
->table('review_media_image')
->select('review_id', 'image_id', 'url')
->whereIn('review_id', $reviewIds)
->where('status_id', 1)
->orderBy('review_id')
->orderBy('image_id')
->get()
->groupBy('review_id');
return $reviews->map(function ($row) use ($imageRows) {
$row->images = $imageRows->get($row->review_id, collect())
->pluck('url')
->toArray();
return $row;
});
}
/**
* Build a map of magento_product_id → bagisto_product_id by joining
* Magento's catalog_product_entity.sku against Bagisto's products.sku.
*
* @return array
*/
private function buildProductIdMap(string $connection): array
{
// Fetch all Magento SKUs.
$magentoPairs = DB::connection($connection)
->table('catalog_product_entity')
->select('entity_id', 'sku')
->get()
->pluck('entity_id', 'sku') // sku → magento_id
->toArray();
if (empty($magentoPairs)) {
return [];
}
// Match against Bagisto's products table.
$skus = array_keys($magentoPairs);
$map = [];
foreach (array_chunk($skus, 500) as $chunk) {
$rows = DB::table('products')
->select('id', 'sku')
->whereIn('sku', $chunk)
->get();
foreach ($rows as $row) {
$magentoId = $magentoPairs[$row->sku] ?? null;
if ($magentoId !== null) {
$map[(int) $magentoId] = (int) $row->id;
}
}
}
return $map;
}
/**
* Build a map of lowercase_email → bagisto_customer_id.
*
* @return array
*/
private function buildCustomerEmailMap(string $connection): array
{
// Fetch Magento customer e-mails.
$magentoEmails = DB::connection($connection)
->table('customer_entity')
->select('email')
->get()
->pluck('email')
->map(fn ($e) => strtolower((string) $e))
->unique()
->toArray();
if (empty($magentoEmails)) {
return [];
}
$map = [];
foreach (array_chunk($magentoEmails, 500) as $chunk) {
$rows = DB::table('customers')
->select('id', 'email')
->whereIn(DB::raw('LOWER(email)'), $chunk)
->get();
foreach ($rows as $row) {
$map[strtolower($row->email)] = (int) $row->id;
}
}
return $map;
}
/**
* Ensure product_reviews has the deduplication column.
* Prints instructions and returns false if the column is absent and
* cannot be auto-created (requires ALTER TABLE permission).
*/
private function ensureMigrationColumnExists(): bool
{
$hasColumn = Schema::hasColumn('product_reviews', 'migrated_from_asteria_id');
if ($hasColumn) {
return true;
}
$this->warn('Column product_reviews.migrated_from_asteria_id is missing.');
$this->line('Run the following migration or SQL:');
$this->line('');
$this->line(' php artisan make:migration add_migrated_from_asteria_id_to_product_reviews');
$this->line(' Then add to the up() method:');
$this->line(' $table->bigInteger(\'migrated_from_asteria_id\')->unsigned()->nullable()->unique();');
$this->line('');
$this->line(' Or directly in MySQL:');
$this->line(' ALTER TABLE product_reviews');
$this->line(' ADD COLUMN migrated_from_asteria_id BIGINT UNSIGNED NULL DEFAULT NULL,');
$this->line(' ADD UNIQUE INDEX uq_migrated_asteria_id (migrated_from_asteria_id);');
$this->line('');
if (! $this->confirm('Attempt to add the column automatically now?', false)) {
return false;
}
try {
DB::statement('
ALTER TABLE product_reviews
ADD COLUMN migrated_from_asteria_id BIGINT UNSIGNED NULL DEFAULT NULL,
ADD UNIQUE INDEX uq_migrated_asteria_id (migrated_from_asteria_id)
');
$this->info('Column added successfully.');
return true;
} catch (\Throwable $e) {
$this->error('Could not add column automatically: '.$e->getMessage());
return false;
}
}
}