diff --git a/src/Catalog/Services/SkuBackfillService.php b/src/Catalog/Services/SkuBackfillService.php index ab7fdd9..56b293d 100644 --- a/src/Catalog/Services/SkuBackfillService.php +++ b/src/Catalog/Services/SkuBackfillService.php @@ -8,15 +8,16 @@ use Lunar\Models\ProductVariant; * Generates a SKU for every ProductVariant missing one — extracted out of * Command\BackfillMissingSkusCommand (which becomes a thin CLI wrapper * around this, keeping --dry-run/progress-bar concerns out of the - * reusable logic) so MigrateImport\RunMigrateImportJob can also call it - * directly, right after a Shopify import, with no CLI concerns at all. + * reusable logic) so MigrateImport\Shopify\Services\ShopifyExportImporter can + * also call it directly, once every product job in its import batch has + * finished (see that class's own import()), with no CLI concerns at all. * * Format is "SKU-P{product_id}-V{variant_id}": deterministic and * guaranteed unique without a uniqueness check, since product_id/ * variant_id already are. Only variants with a null `sku` are touched — * not an importer bug when one shows up after a Shopify import, the * source CSV rows genuinely had no `Variant SKU` value (see - * MigrateImport\Shopify\ShopifyExportImporter). + * MigrateImport\Shopify\Services\ShopifyExportImporter). */ class SkuBackfillService { diff --git a/src/Command/BackfillMissingSkusCommand.php b/src/Command/BackfillMissingSkusCommand.php index 0998d63..1f03e73 100644 --- a/src/Command/BackfillMissingSkusCommand.php +++ b/src/Command/BackfillMissingSkusCommand.php @@ -10,7 +10,7 @@ use Modules\Core\Catalog\Services\SkuBackfillService; * CLI wrapper (--dry-run, a progress bar) around Catalog\Services\ * SkuBackfillService — see that class's own docblock for the actual * backfill logic, also called automatically after a Shopify import (see - * MigrateImport\RunMigrateImportJob). + * MigrateImport\Jobs\RunMigrateImportJob). */ class BackfillMissingSkusCommand extends Command { diff --git a/src/Command/MigrateImportCommand.php b/src/Command/MigrateImportCommand.php index b4df9fd..7f0485e 100644 --- a/src/Command/MigrateImportCommand.php +++ b/src/Command/MigrateImportCommand.php @@ -4,8 +4,8 @@ namespace Modules\Core\Command; use Illuminate\Console\Command; use Lunar\Models\Language; -use Modules\Core\MigrateImport\ImportSpec; -use Modules\Core\MigrateImport\RunMigrateImportJob; +use Modules\Core\MigrateImport\DTOs\ImportSpec; +use Modules\Core\MigrateImport\Jobs\RunMigrateImportJob; class MigrateImportCommand extends Command { @@ -67,7 +67,7 @@ class MigrateImportCommand extends Command // (HTML)/etc. column per row, no per-locale columns at all — so // its text is necessarily written in exactly one language, and // there is no reliable way to detect which one from the file - // itself. Modules\Core\MigrateImport\ImportLocale::code() used to + // itself. Modules\Core\MigrateImport\Services\ImportLocale::code() used to // (as its former name, DefaultLocale, admits) assume it always // matched this store's own Lunar\Models\ // Language::getDefault(), which is often wrong (a store's default diff --git a/src/Command/WipeCatalogCommand.php b/src/Command/WipeCatalogCommand.php index bd5dc5b..8262f45 100644 --- a/src/Command/WipeCatalogCommand.php +++ b/src/Command/WipeCatalogCommand.php @@ -142,7 +142,7 @@ class WipeCatalogCommand extends Command * for the skipped products) whose 'image' ImportMapping rows then * caused a LATER Shopify re-import to silently reuse those now- * orphaned Media objects instead of importing fresh ones — see - * MigrateImport\Shopify\ShopifyExportImporter::resolveOrImportImage()'s + * MigrateImport\Shopify\Services\ShopifyExportImporter::resolveOrImportImage()'s * own docblock for that half of the same incident. Always re-querying * the first N remaining rows (never advancing an id cursor) guarantees * every product is actually visited exactly once, however many are diff --git a/src/MigrateImport/Contracts/Importer.php b/src/MigrateImport/Contracts/Importer.php new file mode 100644 index 0000000..2c628cf --- /dev/null +++ b/src/MigrateImport/Contracts/Importer.php @@ -0,0 +1,10 @@ + $this->spec->source, 'type' => $this->spec->type, 'file' => $this->spec->filePath]); + + $importer = ImporterFactory::make($this->spec); + $importer->import($this->spec); + + Log::info('Import job complete', ['source' => $this->spec->source]); + } +} diff --git a/src/MigrateImport/JudgeMe/Resolvers/ProductResolver.php b/src/MigrateImport/JudgeMe/Resolvers/ProductResolver.php index d1cf3b9..cb1a25e 100644 --- a/src/MigrateImport/JudgeMe/Resolvers/ProductResolver.php +++ b/src/MigrateImport/JudgeMe/Resolvers/ProductResolver.php @@ -3,7 +3,6 @@ namespace Modules\Core\MigrateImport\JudgeMe\Resolvers; use Lunar\Models\Product; -use Lunar\Models\Url; class ProductResolver { diff --git a/src/MigrateImport/JudgeMe/JudgeMeCsvReader.php b/src/MigrateImport/JudgeMe/Services/JudgeMeCsvReader.php similarity index 91% rename from src/MigrateImport/JudgeMe/JudgeMeCsvReader.php rename to src/MigrateImport/JudgeMe/Services/JudgeMeCsvReader.php index b17f90e..085d95d 100644 --- a/src/MigrateImport/JudgeMe/JudgeMeCsvReader.php +++ b/src/MigrateImport/JudgeMe/Services/JudgeMeCsvReader.php @@ -1,6 +1,6 @@ csvReader->read($spec->filePath) as $row) { + $rows = $this->csvReader->read($spec->filePath); + $total = count($rows); + $imported = 0; + + Log::info('JudgeMe import: starting', ['total' => $total]); + + foreach ($rows as $row) { $this->importReview($row); + $imported++; + + if ($imported % 100 === 0) { + Log::info('JudgeMe import: progress', ['imported' => $imported, 'total' => $total]); + } } + + Log::info('JudgeMe import: finished', ['imported' => $imported, 'total' => $total]); } private function importReview(array $row): void diff --git a/src/MigrateImport/RunMigrateImportJob.php b/src/MigrateImport/RunMigrateImportJob.php deleted file mode 100644 index e43123b..0000000 --- a/src/MigrateImport/RunMigrateImportJob.php +++ /dev/null @@ -1,39 +0,0 @@ -spec); - $importer->import($this->spec); - - // Only Shopify's importer creates ProductVariant rows at all (see - // Shopify\ShopifyExportImporter) — JudgeMe never touches products, - // so running this for that source would just be a guaranteed - // no-op query every time. Source CSV rows genuinely can have no - // `Variant SKU` value; see SkuBackfillService's own docblock for - // why that's synthesized rather than treated as an importer bug. - if ($this->spec->source === 'shopify') { - $skuBackfill->backfill(); - } - } -} diff --git a/src/MigrateImport/ImportLocale.php b/src/MigrateImport/Services/ImportLocale.php similarity index 97% rename from src/MigrateImport/ImportLocale.php rename to src/MigrateImport/Services/ImportLocale.php index 396d273..ad33c7b 100644 --- a/src/MigrateImport/ImportLocale.php +++ b/src/MigrateImport/Services/ImportLocale.php @@ -1,6 +1,6 @@ locale !== null) { + ImportLocale::set($this->locale); + } + + try { + $importer->importProduct($this->group, $this->imagesPath, $this->collectionGroup, $this->currency); + } finally { + // A queue worker process outlives a single job — this must + // not leak into whichever product the same worker picks up + // next. + ImportLocale::reset(); + } + } +} diff --git a/src/MigrateImport/Shopify/Resolvers/CollectionResolver.php b/src/MigrateImport/Shopify/Resolvers/CollectionResolver.php index 883ecbe..f20670e 100644 --- a/src/MigrateImport/Shopify/Resolvers/CollectionResolver.php +++ b/src/MigrateImport/Shopify/Resolvers/CollectionResolver.php @@ -7,7 +7,7 @@ use Lunar\FieldTypes\TranslatedText; use Lunar\Models\Collection; use Lunar\Models\CollectionGroup; use Lunar\Models\Product; -use Modules\Core\MigrateImport\ImportLocale; +use Modules\Core\MigrateImport\Services\ImportLocale; class CollectionResolver { diff --git a/src/MigrateImport/Shopify/Resolvers/ImportAttributeResolver.php b/src/MigrateImport/Shopify/Resolvers/ImportAttributeResolver.php index cfebec3..3b2aef8 100644 --- a/src/MigrateImport/Shopify/Resolvers/ImportAttributeResolver.php +++ b/src/MigrateImport/Shopify/Resolvers/ImportAttributeResolver.php @@ -8,7 +8,7 @@ use Lunar\Models\Attribute; use Lunar\Models\AttributeGroup; use Lunar\Models\Product; use Lunar\Models\ProductType; -use Modules\Core\MigrateImport\ImportLocale; +use Modules\Core\MigrateImport\Services\ImportLocale; class ImportAttributeResolver { diff --git a/src/MigrateImport/Shopify/Resolvers/ProductAttributeResolver.php b/src/MigrateImport/Shopify/Resolvers/ProductAttributeResolver.php index 47c3dcb..432ba65 100644 --- a/src/MigrateImport/Shopify/Resolvers/ProductAttributeResolver.php +++ b/src/MigrateImport/Shopify/Resolvers/ProductAttributeResolver.php @@ -6,7 +6,7 @@ use Lunar\FieldTypes\Number; use Lunar\FieldTypes\Text; use Lunar\FieldTypes\TranslatedText; use Lunar\Models\ProductType; -use Modules\Core\MigrateImport\ImportLocale; +use Modules\Core\MigrateImport\Services\ImportLocale; class ProductAttributeResolver { diff --git a/src/MigrateImport/Shopify/Resolvers/ProductOptionResolver.php b/src/MigrateImport/Shopify/Resolvers/ProductOptionResolver.php index 1c24e50..9dcc048 100644 --- a/src/MigrateImport/Shopify/Resolvers/ProductOptionResolver.php +++ b/src/MigrateImport/Shopify/Resolvers/ProductOptionResolver.php @@ -5,7 +5,7 @@ namespace Modules\Core\MigrateImport\Shopify\Resolvers; use Illuminate\Support\Str; use Lunar\Models\ProductOption; use Lunar\Models\ProductOptionValue; -use Modules\Core\MigrateImport\ImportLocale; +use Modules\Core\MigrateImport\Services\ImportLocale; class ProductOptionResolver { diff --git a/src/MigrateImport/Shopify/ShopifyCsvReader.php b/src/MigrateImport/Shopify/Services/ShopifyCsvReader.php similarity index 92% rename from src/MigrateImport/Shopify/ShopifyCsvReader.php rename to src/MigrateImport/Shopify/Services/ShopifyCsvReader.php index ef79352..9f77356 100644 --- a/src/MigrateImport/Shopify/ShopifyCsvReader.php +++ b/src/MigrateImport/Shopify/Services/ShopifyCsvReader.php @@ -1,8 +1,9 @@ locale !== null) { - ImportLocale::set($spec->locale); - } + $groups = $this->csvReader->read($spec->filePath); + $total = count($groups); + $imagesPath = dirname($spec->filePath).'/files'; + $collectionGroup = CollectionGroup::firstOrCreate( + ['handle' => 'shopify'], + ['name' => 'Shopify'], + ); + $currency = Currency::getDefault(); - try { - $groups = $this->csvReader->read($spec->filePath); - $imagesPath = dirname($spec->filePath).'/files'; - $collectionGroup = CollectionGroup::firstOrCreate( - ['handle' => 'shopify'], - ['name' => 'Shopify'], - ); - $currency = Currency::getDefault(); + Log::info('Shopify import: dispatching product jobs', ['total' => $total]); - foreach ($groups as $group) { - $this->importProduct($group, $imagesPath, $collectionGroup, $currency); - } - } finally { - // A queue worker process outlives a single import run — this - // must not leak into whatever's imported next. - ImportLocale::reset(); - } + $jobs = collect($groups)->map(fn (ProductGroup $group) => new ImportShopifyProductJob( + $group, + $imagesPath, + $collectionGroup, + $currency, + $spec->locale, + ))->all(); + + Bus::batch($jobs) + ->name("Shopify import: {$spec->filePath}") + ->then(function () use ($total) { + Log::info('Shopify import: all product jobs finished, backfilling missing SKUs', ['total' => $total]); + app(SkuBackfillService::class)->backfill(); + Log::info('Shopify import: finished', ['total' => $total]); + }) + ->catch(function ($batch, $e) { + Log::error('Shopify import: batch failed', ['error' => $e->getMessage()]); + }) + ->dispatch(); } - private function importProduct( + public function importProduct( ProductGroup $group, string $imagesPath, CollectionGroup $collectionGroup,