Files
core/docs/shopify-reimport.md
T

6.0 KiB

Wiping products before a clean Shopify re-import

A runbook for discarding every imported product (and everything that hangs off one — variants, prices, media, reviews, options/values, the Meilisearch documents) and re-running ShopifyExportImporter from scratch. Useful after a schema/indexer change that only applies to newly-created rows (see "Why a wipe, not an update" below), or when the export CSV itself changed enough that stale products need to go, not just be updated in place.

Every command below is a tinker --execute= one-liner run inside the app container — adjust the exec prefix (./bin/dc-core.sh exec app ..., docker compose exec app ..., etc.) for your setup.


Why a wipe, not an update

ShopifyExportImporter's resolvers are mostly firstOrCreate — re-running the importer against an existing database updates matched rows but leaves already-created ones exactly as they were. That's the right behavior for routine re-imports (an updated price, a new variant), but it means a change to what gets set at creation time only — e.g. ProductOptionResolver now also setting label, not just name, on a ProductOption — never reaches a ProductOption row that already exists. A wipe forces every row to go through creation again, picking up such fixes.


1. Delete every product

Cascades to ProductVariant, prices, and Spatie media rows — verified live (see shopify-import.md's own history/commit log for context). Also removes each product's Meilisearch document automatically, via Scout's own delete hook fired on forceDelete() — no separate scout:flush needed.

\Lunar\Models\Product::withTrashed()->get()->each->forceDelete();

Let this run to completion. Interrupting it mid-loop (e.g. Ctrl+C on the tinker session) stops after whichever product it was on, leaving the rest undeleted — safe to just re-run the same command again afterward, since already-deleted products are simply skipped.

Verify:

\Lunar\Models\Product::withTrashed()->count(); // 0

Requires: product_reviews.product_id cascades on delete

product_reviews (boboko-core's own table, not Lunar's) originally had no ON DELETE clause on its product_id foreign key — deleting a reviewed product threw a constraint violation instead of the review going with it. Fixed by database/migrations/2026_09_03_000001_add_cascade_delete_to_product_reviews_product_id.php. Make sure this migration has actually run (php artisan migrate) before step 1, or a product with reviews will fail to delete.


2. Delete product options and values

Not touched by step 1 (ProductOption/ProductOptionValue aren't scoped to one product — they're shared across the catalog, per ProductOptionResolver::resolveOption()'s shared: true). Safe to delete in full once every product (and therefore every variant referencing an option value via the product_option_value_product_variant pivot) is gone — deleting values while variants still reference them throws the same kind of FK violation step 1 guards against.

\Lunar\Models\ProductOptionValue::query()->delete();
\Lunar\Models\ProductOption::query()->delete();

Verify:

\Lunar\Models\ProductOption::count();      // 0
\Lunar\Models\ProductOptionValue::count(); // 0

3. Clear the import mappings

Without this, the importer's ImportMapping::resolve(...) calls still find the (now-deleted) mappings' rows absent, so this step is really about not leaving stale mapping rows pointing at nothing — ImportMapping rows aren't foreign-keyed to the models they map (morphTo, no constraint), so leaving them wouldn't break the re-import, but a stale mapping for a product that no longer exists is dead weight.

\Modules\Core\MigrateImport\Models\ImportMapping::where('source', 'shopify')->delete();

Verify:

\Modules\Core\MigrateImport\Models\ImportMapping::where('source', 'shopify')->count(); // 0

4. Re-run the importer

boboko:migrate:import dispatches RunMigrateImportJob onto the queue — not synchronous — so a queue worker must actually be running (php artisan queue:work, or your dev queue container) or the job just sits queued.

php artisan boboko:migrate:import --source=shopify --type=export --file=<absolute path to the CSV>

The --file value must be an absolute path inside the container (e.g. /var/www/html/storage/app/private/imports/shopify/products_export.csv) when running non-interactively — a path relative to storage/app/private/imports only resolves correctly when the command can fall back to its interactive prompt, which isn't available in a scripted/non-TTY run.

Watch the queue worker's own log output for FAIL entries (see docs/lunar.md or your compose setup for how logs are routed to docker compose logs) — a clean run shows every Laravel\Scout\Jobs\MakeSearchable / Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob line ending DONE, never FAIL.


5. Re-sync Meilisearch and reindex

php artisan lunar:meilisearch:setup
php artisan lunar:meilisearch:tune-product-search
php artisan lunar:search:index "Lunar\Models\Product" --refresh

--refresh re-syncs filterable/sortable index settings and reindexes every document — it does not reset typoTolerance/prefixSearch (confirmed live: both survived a --refresh run unchanged), so tune-product-search only needs re-running here for completeness/if it hadn't already been applied, not because --refresh would have clobbered it.


Verifying the result

// Product count should match the CSV's actual unique `Handle` count, not
// whatever the database held before the wipe — those aren't the same number
// if stale/manually-added products existed alongside the CSV-sourced ones.
\Lunar\Models\Product::count();

// Spot-check that at least one variant picked up its own image (see
// shopify-import.md's "Images" section) — 0 is only correct if the CSV
// genuinely has no `Variant Image` values populated.
\Lunar\Models\ProductVariant::has('images')->count();