*/ private array $channelCodes = []; private bool $hasUniqueId = false; private bool $hasJsonValue = false; private bool $hasProductFlat = false; private int $scanned = 0; private int $disabled = 0; private int $unchanged = 0; private int $inStock = 0; private int $noStockRow = 0; /** @var array */ private array $touchedProductIds = []; /** @var array */ private array $examples = []; public function handle(): int { $dryRun = (bool) $this->option('dry-run'); $this->connectionName = (string) $this->option('connection'); $sku = trim((string) $this->option('sku')); try { DB::connection($this->connectionName)->getPdo(); } catch (\Throwable $e) { $this->error("Cannot connect to Asteria DB (connection='{$this->connectionName}'): ".$e->getMessage()); return self::FAILURE; } $schema = new Magento1Schema($this->connectionName); if (! $schema->hasTable('cataloginventory_stock_item')) { $this->error("Asteria table 'cataloginventory_stock_item' is missing on connection '{$this->connectionName}'."); return self::FAILURE; } $columns = $schema->existingColumns('cataloginventory_stock_item', [ 'product_id', 'is_in_stock', 'stock_id', ]); if (! in_array('product_id', $columns, true) || ! in_array('is_in_stock', $columns, true)) { $this->error('cataloginventory_stock_item is missing product_id or is_in_stock.'); return self::FAILURE; } $this->hasStockId = in_array('stock_id', $columns, true); if (! Schema::hasColumn('products', 'migrated_from_asteria_id')) { $this->error('products.migrated_from_asteria_id is missing. Run php artisan migrate.'); return self::FAILURE; } $statusAttributeId = DB::table('attributes')->where('code', 'status')->value('id'); if (! $statusAttributeId) { $this->error('Bagisto status attribute is missing.'); return self::FAILURE; } $this->statusAttributeId = (int) $statusAttributeId; $this->channelCodes = DB::table('channels')->orderBy('id')->pluck('code') ->map(fn ($code) => trim((string) $code)) ->filter() ->unique() ->values() ->all(); if ($this->channelCodes === []) { $this->channelCodes = ['default']; } $this->hasUniqueId = Schema::hasColumn('product_attribute_values', 'unique_id'); $this->hasJsonValue = Schema::hasColumn('product_attribute_values', 'json_value'); $this->hasProductFlat = Schema::hasTable('product_flat') && Schema::hasColumn('product_flat', 'status'); $query = Product::query() ->whereNotNull('migrated_from_asteria_id') ->select('id', 'sku', 'migrated_from_asteria_id') ->orderBy('id'); if ($sku !== '') { $query->where('sku', $sku); if (! (clone $query)->exists()) { $exists = Product::query()->where('sku', $sku)->exists(); $this->error($exists ? "Product {$sku} was not migrated from Asteria." : "Product {$sku} was not found."); return self::FAILURE; } } $query->chunkById(200, function (Collection $products) use ($dryRun) { $this->repairChunk($products, $dryRun); }); if ($this->examples !== []) { $this->table(['sku', 'product_id'], $this->examples); } if (! $dryRun && ! $this->option('no-index')) { $this->reindexProducts(); } $this->info(sprintf( '%s scanned=%d disabled=%d unchanged=%d in_stock=%d no_stock_row=%d', $dryRun ? '[DRY RUN]' : 'Done.', $this->scanned, $this->disabled, $this->unchanged, $this->inStock, $this->noStockRow )); return self::SUCCESS; } private function repairChunk(Collection $products, bool $dryRun): void { $asteriaIds = $products ->pluck('migrated_from_asteria_id') ->map(fn ($id) => (int) $id) ->unique() ->values() ->all(); $stock = $this->loadStock($asteriaIds); $outOfStock = $products->filter(function ($product) use ($stock) { $asteriaId = (int) $product->migrated_from_asteria_id; return array_key_exists($asteriaId, $stock) && $stock[$asteriaId] === false; })->values(); foreach ($products as $product) { $this->scanned++; $asteriaId = (int) $product->migrated_from_asteria_id; if (! array_key_exists($asteriaId, $stock)) { $this->noStockRow++; } elseif ($stock[$asteriaId] === true) { $this->inStock++; } } if ($outOfStock->isEmpty()) { return; } $productIds = $outOfStock->pluck('id')->map(fn ($id) => (int) $id)->all(); $statusRows = DB::table('product_attribute_values') ->where('attribute_id', $this->statusAttributeId) ->whereIn('product_id', $productIds) ->get(['id', 'product_id', 'channel', 'boolean_value']); $rowsByProduct = []; foreach ($statusRows as $row) { $rowsByProduct[(int) $row->product_id][] = $row; } $flatNeedsUpdate = []; if ($this->hasProductFlat) { $flatNeedsUpdate = DB::table('product_flat') ->whereIn('product_id', $productIds) ->where(function ($query) { $query->where('status', '!=', 0)->orWhereNull('status'); }) ->pluck('product_id') ->mapWithKeys(fn ($id) => [(int) $id => true]) ->all(); } $changedIds = []; foreach ($outOfStock as $product) { $productId = (int) $product->id; $rows = $rowsByProduct[$productId] ?? []; $presentChannels = []; $statusOff = $rows !== []; foreach ($rows as $row) { if ((int) $row->boolean_value !== 0) { $statusOff = false; } $channel = trim((string) ($row->channel ?? '')); if ($channel !== '') { $presentChannels[$channel] = true; } } $missingChannel = false; foreach ($this->channelCodes as $channel) { if (! isset($presentChannels[$channel])) { $missingChannel = true; break; } } if ($statusOff && ! $missingChannel && ! isset($flatNeedsUpdate[$productId])) { $this->unchanged++; continue; } $this->disabled++; $changedIds[] = $productId; if (count($this->examples) < 20) { $this->examples[] = [(string) $product->sku, $productId]; } } if ($changedIds === [] || $dryRun) { return; } $this->touchedProductIds = array_merge($this->touchedProductIds, $changedIds); DB::table('product_attribute_values') ->where('attribute_id', $this->statusAttributeId) ->whereIn('product_id', $changedIds) ->where(function ($query) { $query->where('boolean_value', '!=', 0)->orWhereNull('boolean_value'); }) ->update(['boolean_value' => 0]); $inserts = []; foreach ($changedIds as $productId) { $presentChannels = []; foreach ($rowsByProduct[$productId] ?? [] as $row) { $channel = trim((string) ($row->channel ?? '')); if ($channel !== '') { $presentChannels[$channel] = true; } } foreach ($this->channelCodes as $channel) { if (isset($presentChannels[$channel])) { continue; } $inserts[] = $this->statusValueRow($productId, $channel); } } foreach (array_chunk($inserts, 500) as $chunk) { DB::table('product_attribute_values')->insert($chunk); } if ($this->hasProductFlat) { $flatUpdate = ['status' => 0]; if (Schema::hasColumn('product_flat', 'updated_at')) { $flatUpdate['updated_at'] = now(); } DB::table('product_flat') ->whereIn('product_id', $changedIds) ->update($flatUpdate); } } /** * @param array $asteriaIds * @return array asteria id => true when In Stock */ private function loadStock(array $asteriaIds): array { if ($asteriaIds === []) { return []; } $query = DB::connection($this->connectionName) ->table('cataloginventory_stock_item') ->whereIn('product_id', $asteriaIds); if ($this->hasStockId) { $query->where('stock_id', 1); } $stock = []; foreach ($query->get(['product_id', 'is_in_stock']) as $row) { $stock[(int) $row->product_id] = (int) $row->is_in_stock === 1; } return $stock; } /** * @return array */ private function statusValueRow(int $productId, string $channel): array { $row = [ 'locale' => null, 'channel' => $channel, 'text_value' => null, 'boolean_value' => 0, 'integer_value' => null, 'float_value' => null, 'datetime_value'=> null, 'date_value' => null, 'product_id' => $productId, 'attribute_id' => $this->statusAttributeId, ]; if ($this->hasJsonValue) { $row['json_value'] = null; } if ($this->hasUniqueId) { $row['unique_id'] = $channel.'|'.$productId.'|'.$this->statusAttributeId; } return $row; } private function reindexProducts(): void { $ids = array_values(array_unique($this->touchedProductIds)); if ($ids === [] || ! $this->elasticEnabled()) { return; } $this->info('Reindexing '.count($ids).' products…'); $elasticIndexer = app(ElasticSearch::class); foreach (array_chunk($ids, 20) as $chunk) { $products = Product::query()->with([ 'attribute_family', 'attribute_values', 'channels', 'categories', ])->whereIn('id', $chunk)->get(); foreach ($products as $product) { try { $elasticIndexer->reindexRow($product); } catch (\Throwable $e) { $this->error('Reindex failed for product '.$product->sku.': '.$e->getMessage()); } } } } private function elasticEnabled(): bool { return core()->getConfigData('catalog.products.search.engine') === 'elastic'; } }