Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config/shopify.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions docs/content/en/CMS/importing-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions src/Jobs/ImportSingleProductJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
112 changes: 112 additions & 0 deletions tests/Unit/ImportSingleProductJobTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 '{
Expand Down
Loading