From c16f295642a627192b8cfc93529aed0388a0a290 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Tue, 17 Mar 2026 06:20:44 +0000 Subject: [PATCH 1/2] Add `import_all_products` config to allow import to be limited to sales channel --- config/shopify.php | 7 ++ src/Jobs/ImportSingleProductJob.php | 14 +++ tests/Unit/ImportSingleProductJobTest.php | 112 ++++++++++++++++++++++ 3 files changed, 133 insertions(+) diff --git a/config/shopify.php b/config/shopify.php index 1a3e992..f994d1f 100644 --- a/config/shopify.php +++ b/config/shopify.php @@ -128,6 +128,13 @@ */ 'sales_channel' => env('SHOPIFY_SALES_CHANNEL', 'Online Store'), + /** + * When true (default), all products are imported regardless of sales channel membership. + * When false, only products assigned to the configured sales_channel are imported. + * Products previously imported that are no longer on the sales channel will be deleted. + */ + 'import_all_products' => true, + /** * Your App's Admin API Auth Key * Note: this is not required unless you are doing custom integrations using the RestApi diff --git a/src/Jobs/ImportSingleProductJob.php b/src/Jobs/ImportSingleProductJob.php index a87f4a9..660af70 100644 --- a/src/Jobs/ImportSingleProductJob.php +++ b/src/Jobs/ImportSingleProductJob.php @@ -131,6 +131,20 @@ public function handle() ->where('product_id', $this->data['id']) ->first(); + if (! config('shopify.import_all_products', true)) { + $inSalesChannel = collect(Arr::get($this->data, 'resourcePublications.edges', [])) + ->contains(fn ($pub) => Arr::get($pub, 'node.publication.name') === config('shopify.sales_channel', 'Online Store')); + + if (! $inSalesChannel) { + if ($entry) { + $this->removeOldVariants([], $this->data['handle']); + $entry->delete(); + } + + return; + } + } + // Clean up data whilst checking if product exists $tags = $this->importTaxonomy($this->data['tags'], config('shopify.taxonomies.tags')); $vendors = $this->importTaxonomy([$this->data['vendor']], config('shopify.taxonomies.vendor')); diff --git a/tests/Unit/ImportSingleProductJobTest.php b/tests/Unit/ImportSingleProductJobTest.php index 36fcd1c..f5ba85d 100644 --- a/tests/Unit/ImportSingleProductJobTest.php +++ b/tests/Unit/ImportSingleProductJobTest.php @@ -352,6 +352,118 @@ public function clears_variant_metafields_removed_in_shopify() $this->assertSame([], $variant->get('shopify_metafield_keys')); } + #[Test] + public function skips_import_when_not_in_sales_channel() + { + config(['shopify.import_all_products' => false]); + + Facades\Collection::make(config('shopify.collection_handle', 'products'))->save(); + Facades\Collection::make('variants')->save(); + Facades\Taxonomy::make()->handle('collections')->save(); + Facades\Taxonomy::make()->handle('tags')->save(); + Facades\Taxonomy::make()->handle('type')->save(); + Facades\Taxonomy::make()->handle('vendor')->save(); + + $this->mock(Graphql::class, function (MockInterface $mock) { + $mock->shouldReceive('query')->andReturn(new HttpResponse( + status: 200, + body: $this->getProductJson() + )); + }); + + Jobs\ImportSingleProductJob::dispatch(1); + + $this->assertCount(0, Facades\Entry::whereCollection(config('shopify.collection_handle', 'products'))); + $this->assertCount(0, Facades\Entry::whereCollection('variants')); + } + + #[Test] + public function deletes_existing_product_when_removed_from_sales_channel() + { + config(['shopify.import_all_products' => false]); + + Facades\Collection::make(config('shopify.collection_handle', 'products'))->save(); + Facades\Collection::make('variants')->save(); + Facades\Taxonomy::make()->handle('collections')->save(); + Facades\Taxonomy::make()->handle('tags')->save(); + Facades\Taxonomy::make()->handle('type')->save(); + Facades\Taxonomy::make()->handle('vendor')->save(); + + // First import with the product in the sales channel + $jsonWithChannel = str_replace('"resourcePublications": {}', '"resourcePublications": { + "edges": [ + { + "node": { + "isPublished": true, + "publication": { + "id": "gid://shopify/Publication/1", + "name": "Online Store" + }, + "publishDate": "2024-01-01T00:00:00Z" + } + } + ] + }', $this->getProductJson()); + + $this->mock(Graphql::class, function (MockInterface $mock) use ($jsonWithChannel) { + $mock->shouldReceive('query')->andReturn( + new HttpResponse(status: 200, body: $jsonWithChannel), + new HttpResponse(status: 200, body: $this->getProductJson()) + ); + }); + + Jobs\ImportSingleProductJob::dispatch(1); + + $this->assertCount(1, Facades\Entry::whereCollection(config('shopify.collection_handle', 'products'))); + $this->assertCount(1, Facades\Entry::whereCollection('variants')); + + // Second import with the product no longer in the sales channel + Jobs\ImportSingleProductJob::dispatch(1); + + $this->assertCount(0, Facades\Entry::whereCollection(config('shopify.collection_handle', 'products'))); + $this->assertCount(0, Facades\Entry::whereCollection('variants')); + } + + #[Test] + public function imports_product_when_in_sales_channel_and_filter_enabled() + { + config(['shopify.import_all_products' => false]); + + Facades\Collection::make(config('shopify.collection_handle', 'products'))->save(); + Facades\Taxonomy::make()->handle('collections')->save(); + Facades\Taxonomy::make()->handle('tags')->save(); + Facades\Taxonomy::make()->handle('type')->save(); + Facades\Taxonomy::make()->handle('vendor')->save(); + + $jsonWithChannel = str_replace('"resourcePublications": {}', '"resourcePublications": { + "edges": [ + { + "node": { + "isPublished": true, + "publication": { + "id": "gid://shopify/Publication/1", + "name": "Online Store" + }, + "publishDate": "2024-01-01T00:00:00Z" + } + } + ] + }', $this->getProductJson()); + + $this->mock(Graphql::class, function (MockInterface $mock) use ($jsonWithChannel) { + $mock->shouldReceive('query')->andReturn(new HttpResponse( + status: 200, + body: $jsonWithChannel + )); + }); + + Jobs\ImportSingleProductJob::dispatch(1); + + $entry = Facades\Entry::whereCollection(config('shopify.collection_handle', 'products'))->first(); + $this->assertNotNull($entry); + $this->assertSame($entry->product_id, '108828309'); + } + private function getProductJson(): string { return '{ From f33ebe12b952886f3b692d83070addff02afc205 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Tue, 17 Mar 2026 06:21:54 +0000 Subject: [PATCH 2/2] Add docs --- docs/content/en/CMS/importing-data.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/content/en/CMS/importing-data.md b/docs/content/en/CMS/importing-data.md index 04b85c7..aefa738 100644 --- a/docs/content/en/CMS/importing-data.md +++ b/docs/content/en/CMS/importing-data.md @@ -99,6 +99,16 @@ Alt text is imported from Shopify alongside each product and variant image. When By default the `published` state and `published_at` of the product is determined by the values of `Online Store` sales channel. If you want to use a different sales channel to determine availability you can specify the name of the channel in the `SHOPIFY_SALES_CHANNEL` env variable, e.g. `SHOPIFY_SALES_CHANNEL="My other channel"`. +## Sales channel filtering + +By default all Shopify products are imported regardless of whether they are assigned to the configured sales channel. To restrict imports to only products on the sales channel, set `import_all_products` to `false` in `config/shopify.php`: + +```php +'import_all_products' => false, +``` + +When this is enabled, any product that is not assigned to the configured sales channel will be skipped. If a product was previously imported and is later removed from the sales channel, it and all of its variants will be deleted from Statamic on the next import. + ## Metafields Any product and variant meta fields will be automatically added to the Statamic entry data, with the same handle as their key in Shopify and using the raw value.