diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index d2c57ff7..888af6ee 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -1,3 +1,25 @@ # WIP Release Notes for Shopify 8.0 -- Shopify for Craft now requires Craft CMS 5.10.7 or later. \ No newline at end of file +### Extensibility + +- Added `craft\shopify\models\BulkOperation::$shopifyGid`. +- Added `craft\shopify\models\Variant::$shopifyGid`. +- Added `craft\shopify\jobs\ProcessBulkOperationData::$bulkOperationShopifyGid`. +- Added `craft\shopify\services\BulkOperations::getBulkOperationByShopifyGid()`. +- Added `craft\shopify\services\Products::deleteProductByShopifyGid()`. +- Added `craft\shopify\services\Products::deleteShopifyDataByShopifyGid()`. +- Added `craft\shopify\services\Products::syncProductByShopifyGid()`. +- `craft\shopify\models\Variant::$shopifyId` now holds the numeric Shopify ID. The full GID is now available via `$shopifyGid`. +- `craft\shopify\records\ShopifyData::$shopifyId` is now a generated (read-only) column containing the numeric Shopify ID. The full GID is now available via `$shopifyGid`. +- Renamed `craft\shopify\jobs\ProcessBulkOperationData::$bulkOperationShopifyId` to `$bulkOperationShopifyGid`. +- Renamed `craft\shopify\models\BulkOperation::$shopifyId` to `$shopifyGid`. +- Deprecated `craft\shopify\services\BulkOperations::getBulkOperationByShopifyId()`. Use `getBulkOperationByShopifyGid()` instead. +- Deprecated `craft\shopify\services\Products::deleteProductByShopifyId()`. Use `deleteProductByShopifyGid()` instead. +- Deprecated `craft\shopify\services\Products::deleteShopifyDataByShopifyId()`. Use `deleteShopifyDataByShopifyGid()` instead. +- Deprecated `craft\shopify\services\Products::syncProductByShopifyId()`. Use `syncProductByShopifyGid()` instead. + +### System + +- The `shopify_data` table's `shopifyId` column has been renamed to `shopifyGid`. A new generated `shopifyId` column (the numeric ID at the end of the GID) has been added. +- The `shopify_bulkoperations` table's `shopifyId` column has been renamed to `shopifyGid`. +- Shopify for Craft now requires Craft CMS 5.10.7 or later. diff --git a/composer.lock b/composer.lock index 5ca2dbe1..b7b8331d 100644 --- a/composer.lock +++ b/composer.lock @@ -538,16 +538,16 @@ }, { "name": "craftcms/cms", - "version": "5.10.7", + "version": "5.10.8", "source": { "type": "git", "url": "https://github.com/craftcms/cms.git", - "reference": "6a08356668f772f0a4d79769d3de83508cd4e21c" + "reference": "ec387a529a7f9a0c07f2a086c59dadc02fc79556" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms/zipball/6a08356668f772f0a4d79769d3de83508cd4e21c", - "reference": "6a08356668f772f0a4d79769d3de83508cd4e21c", + "url": "https://api.github.com/repos/craftcms/cms/zipball/ec387a529a7f9a0c07f2a086c59dadc02fc79556", + "reference": "ec387a529a7f9a0c07f2a086c59dadc02fc79556", "shasum": "" }, "require": { @@ -664,7 +664,7 @@ "rss": "https://github.com/craftcms/cms/releases.atom", "source": "https://github.com/craftcms/cms" }, - "time": "2026-06-18T01:14:32+00:00" + "time": "2026-06-23T11:02:47+00:00" }, { "name": "craftcms/plugin-installer", diff --git a/src/Plugin.php b/src/Plugin.php index 9e186b4f..9a6eee2b 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -79,7 +79,7 @@ class Plugin extends BasePlugin /** * @var string */ - public string $schemaVersion = '7.1.0.0'; + public string $schemaVersion = '8.0.0'; /** * @inheritdoc @@ -450,8 +450,8 @@ private function _registerGarbageCollection(): void 'products.shopifyId', ]) ->from(Table::PRODUCTS . ' products') - ->leftJoin(Table::DATA . ' data', '[[data.shopifyId]] = [[products.shopifyGid]]') - ->where(['data.shopifyId' => null]) + ->leftJoin(Table::DATA . ' data', '[[data.shopifyGid]] = [[products.shopifyGid]]') + ->where(['data.shopifyGid' => null]) ->all(); $shopifyIds = ArrayHelper::getColumn($shopifyProductElementsMissingData, 'shopifyId'); diff --git a/src/collections/VariantCollection.php b/src/collections/VariantCollection.php index a489f649..fd381c32 100644 --- a/src/collections/VariantCollection.php +++ b/src/collections/VariantCollection.php @@ -42,6 +42,7 @@ public static function make($items = []) $item = Craft::createObject([ 'class' => Variant::class, 'id' => $item->id, + 'shopifyGid' => $item->shopifyGid, 'shopifyId' => $item->shopifyId, 'type' => $item->type, 'parentId' => $item->parentId, diff --git a/src/elements/Product.php b/src/elements/Product.php index 71a2299e..7a467c20 100644 --- a/src/elements/Product.php +++ b/src/elements/Product.php @@ -23,6 +23,7 @@ use craft\shopify\fieldlayoutelements\MetafieldsField; use craft\shopify\fieldlayoutelements\OptionsField; use craft\shopify\fieldlayoutelements\VariantsField; +use craft\shopify\helpers\Metafield as MetafieldHelper; use craft\shopify\helpers\Product as ProductHelper; use craft\shopify\models\Variant; use craft\shopify\Plugin; @@ -359,8 +360,9 @@ public function getOptions(): array } /** - * @param string|array $value + * @param string|array $value A list-shaped array of `{key, value}` objects, or a JSON-encoded string of the same. * @return void + * @throws \InvalidArgumentException if the value is not a list-shaped array or JSON string of one. */ public function setMetafields(string|array $value): void { @@ -368,7 +370,11 @@ public function setMetafields(string|array $value): void $value = Json::decodeIfJson($value); } - $this->_metaFields = $value; + if (!is_array($value) || !array_is_list($value)) { + throw new \InvalidArgumentException('setMetafields() expects a list-shaped array of {key, value} objects or a JSON-encoded string of the same.'); + } + + $this->_metaFields = MetafieldHelper::normalizeToMap($value); } /** @@ -387,14 +393,7 @@ public function getMetafields(): array $data = Plugin::getInstance()->getApi()->getShopifyDataByType('Metafield', $this->shopifyGid); - $metafields = $data - ->mapWithKeys(function($d) { - return [ - $d['key'] => Json::decodeIfJson($d['value']), - ]; - }); - - $this->setMetafields($metafields); + $this->setMetafields($data->all()); return $this->_metaFields ?? []; } @@ -814,7 +813,7 @@ public function afterDelete(): void { // Remove all the product shopify data if ($this->shopifyGid && $this->getIsCanonical()) { - Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyId($this->shopifyGid); + Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyGid($this->shopifyGid); } parent::afterDelete(); diff --git a/src/elements/db/ProductQuery.php b/src/elements/db/ProductQuery.php index 66fc57bb..8f295c6c 100644 --- a/src/elements/db/ProductQuery.php +++ b/src/elements/db/ProductQuery.php @@ -396,8 +396,8 @@ protected function beforePrepare(): bool // join standard product element table that only contains the shopifyId $this->joinElementTable('shopify_products'); - $this->query->innerJoin(Table::DATA . ' data', new Expression('[[data.shopifyId]] = [[shopify_products.shopifyGid]]')); - $this->subQuery->innerJoin(Table::DATA . ' data', new Expression('[[data.shopifyId]] = [[shopify_products.shopifyGid]]')); + $this->query->innerJoin(Table::DATA . ' data', new Expression('[[data.shopifyGid]] = [[shopify_products.shopifyGid]]')); + $this->subQuery->innerJoin(Table::DATA . ' data', new Expression('[[data.shopifyGid]] = [[shopify_products.shopifyGid]]')); $this->query->select([ 'shopify_products.shopifyId', diff --git a/src/fieldlayoutelements/VariantsField.php b/src/fieldlayoutelements/VariantsField.php index 46511a23..e38c5147 100644 --- a/src/fieldlayoutelements/VariantsField.php +++ b/src/fieldlayoutelements/VariantsField.php @@ -63,7 +63,7 @@ protected function inputHtml(ElementInterface $element = null, bool $static = fa ]; foreach ($variants as $variant) { - $link = sprintf('%s/variants/%s', $element->getShopifyEditUrl(), str_replace('gid://shopify/ProductVariant/', '', $variant->shopifyId)); + $link = sprintf('%s/variants/%s', $element->getShopifyEditUrl(), $variant->shopifyId); $title = $variant->title; $sku = $variant->sku; diff --git a/src/gql/types/Variant.php b/src/gql/types/Variant.php index b80276cc..96579ceb 100644 --- a/src/gql/types/Variant.php +++ b/src/gql/types/Variant.php @@ -56,13 +56,11 @@ public static function getFieldDefinitions(): array 'name' => 'shopifyId', 'type' => Type::string(), 'description' => 'Shopify ID of the variant.', - 'resolve' => fn(VariantElement $source) => str_replace('gid://shopify/ProductVariant/', '', $source->shopifyId), ], 'shopifyGid' => [ 'name' => 'shopifyGid', 'type' => Type::string(), 'description' => 'Shopify GID of the variant.', - 'resolve' => fn(VariantElement $source) => $source->shopifyId, ], 'title' => [ 'name' => 'title', diff --git a/src/handlers/Webhook.php b/src/handlers/Webhook.php index 1b31c923..700944d8 100644 --- a/src/handlers/Webhook.php +++ b/src/handlers/Webhook.php @@ -24,10 +24,10 @@ public function handle(string $topic, string $shop, array $body): void switch ($topic) { case Topics::PRODUCTS_UPDATE: case Topics::PRODUCTS_CREATE: - Plugin::getInstance()->getProducts()->syncProductByShopifyId($body['id']); + Plugin::getInstance()->getProducts()->syncProductByShopifyGid($body['id']); break; case Topics::PRODUCTS_DELETE: - Plugin::getInstance()->getProducts()->deleteProductByShopifyId($body['id']); + Plugin::getInstance()->getProducts()->deleteProductByShopifyGid($body['id']); break; case Topics::INVENTORY_ITEMS_UPDATE: Plugin::getInstance()->getProducts()->syncProductByInventoryItemId($body['admin_graphql_api_id']); diff --git a/src/helpers/Metafield.php b/src/helpers/Metafield.php new file mode 100644 index 00000000..ba243427 --- /dev/null +++ b/src/helpers/Metafield.php @@ -0,0 +1,53 @@ + + * @since 8.0.0 + */ +class Metafield +{ + /** + * Normalizes an iterable of metafield data into a flat key => value map. + * + * Accepts either: + * - [[ShopifyData]] ActiveRecord objects (from [[Api::getShopifyDataByType()]] with `$returnRecords = true`), + * where each record's `data` column holds a JSON-encoded `{key, value}` object. + * - Pre-decoded associative arrays (from [[Api::getShopifyDataByType()]] without `$returnRecords`), + * where each item already has `key` and `value` keys. + * + * Rows that do not carry both `key` and `value` are silently skipped. + * + * @param iterable $rows + * @return array + */ + public static function normalizeToMap(iterable $rows): array + { + return collect($rows) + ->mapWithKeys(function($d) { + $data = match (true) { + $d instanceof ShopifyData => Json::decodeIfJson($d->data), + is_string($d) => Json::decodeIfJson($d), + default => $d, + }; + + if (!is_array($data) || !isset($data['key']) || !isset($data['value'])) { + return []; + } + + return [$data['key'] => Json::decodeIfJson($data['value'])]; + }) + ->all(); + } +} diff --git a/src/helpers/Product.php b/src/helpers/Product.php index 063ed1d8..eb4ac17f 100644 --- a/src/helpers/Product.php +++ b/src/helpers/Product.php @@ -131,7 +131,7 @@ public static function renderCardHtml(ProductElement $product, array $excludeMet // This is the date updated in the database which represents the last time it was updated from a Shopify webhook or sync. /** @var ShopifyData $productData */ - $productData = ShopifyData::find()->where(['shopifyId' => $product->shopifyGid])->one(); + $productData = ShopifyData::find()->where(['shopifyGid' => $product->shopifyGid])->one(); $dateUpdated = DateTimeHelper::toDateTime($productData->dateUpdated); $now = new \DateTime(); $diff = $now->diff($dateUpdated); diff --git a/src/jobs/ProcessBulkOperationData.php b/src/jobs/ProcessBulkOperationData.php index 0799c122..159d2fe9 100644 --- a/src/jobs/ProcessBulkOperationData.php +++ b/src/jobs/ProcessBulkOperationData.php @@ -23,7 +23,7 @@ class ProcessBulkOperationData extends BaseBatchedJob /** * @var string */ - public string $bulkOperationShopifyId; + public string $bulkOperationShopifyGid = ''; /** * @var string @@ -110,7 +110,7 @@ protected function processItem(mixed $item): void // Find data records based on their Shopify ID and parent ID. This is to avoid overwriting // records with the same ID but different parents (e.g. Metafields, Images etc). - $record = ShopifyData::findOne(['shopifyId' => $item['id'], 'parentId' => $parentId]); + $record = ShopifyData::findOne(['shopifyGid' => $item['id'], 'parentId' => $parentId]); if (!$record) { $record = new ShopifyData(); } @@ -119,7 +119,7 @@ protected function processItem(mixed $item): void $parts = explode('/', str_replace('gid://shopify/', '', $item['id'])); $type = $parts[0]; - $record->shopifyId = $item['id']; + $record->shopifyGid = $item['id']; $record->type = $type; $record->data = $item; $record->parentId = $item['__parentId'] ?? null; @@ -139,7 +139,7 @@ protected function before(): void parent::before(); // Make sure bulk op is marked as processing - $bulkOperation = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyId($this->bulkOperationShopifyId); + $bulkOperation = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyGid($this->bulkOperationShopifyGid); if (!$bulkOperation) { return; @@ -153,7 +153,7 @@ protected function before(): void if ($this->clearData === BulkOperationRecord::CLEAR_DATA_ALL) { ShopifyData::deleteAll(); } elseif ($this->clearData !== BulkOperationRecord::CLEAR_DATA_NONE) { - Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyId($this->clearData); + Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyGid($this->clearData); } } @@ -165,7 +165,7 @@ protected function after(): void parent::after(); // Mark bulk op as completed - $bulkOperation = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyId($this->bulkOperationShopifyId); + $bulkOperation = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyGid($this->bulkOperationShopifyGid); if (!$bulkOperation) { return; diff --git a/src/migrations/Install.php b/src/migrations/Install.php index fffe0c93..66ccf78b 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -57,7 +57,7 @@ public function createTables(): void $this->archiveTableIfExists(Table::DATA); $this->createTable(Table::DATA, [ 'id' => $this->primaryKey(), - 'shopifyId' => $this->string(), + 'shopifyGid' => $this->string(), 'type' => $this->string(), 'data' => $this->json(), 'parentId' => $this->string(), @@ -69,7 +69,7 @@ public function createTables(): void $this->archiveTableIfExists(Table::BULK_OPERATIONS); $this->createTable(Table::BULK_OPERATIONS, [ 'id' => $this->primaryKey(), - 'shopifyId' => $this->string(), + 'shopifyGid' => $this->string(), 'url' => $this->text(), 'objectCount' => $this->integer(), 'query' => $this->text(), @@ -139,6 +139,17 @@ public function createGeneratedColumns(): void $db->quoteColumnName($alias) . ' ' . $qb->getColumnType($this->integer()) . " GENERATED ALWAYS AS (" . $expression . ") STORED;"); } + + // Derived shopifyId — the numeric ID at the end of the GID (e.g. "7136060145715" from "gid://shopify/Product/7136060145715") + if ($db->getIsPgsql()) { + $shopifyIdExpression = "regexp_replace(\"shopifyGid\", '^.*/', '')"; + } else { + $shopifyIdExpression = "SUBSTRING_INDEX(`shopifyGid`, '/', -1)"; + } + + $this->execute("ALTER TABLE " . Table::DATA . " ADD COLUMN " . + $db->quoteColumnName('shopifyId') . ' ' . $qb->getColumnType($this->string()) . " GENERATED ALWAYS AS (" . + $shopifyIdExpression . ") STORED;"); } /** @@ -148,7 +159,7 @@ public function createIndexes(): void { $this->createIndex(null, Table::PRODUCTS, ['shopifyId'], false); $this->createIndex(null, Table::PRODUCTS, ['shopifyGid'], false); - $this->createIndex(null, Table::DATA, ['shopifyId'], false); + $this->createIndex(null, Table::DATA, ['shopifyGid'], false); $this->createIndex(null, Table::DATA, ['parentId'], false); } diff --git a/src/migrations/m260617_100000_rename_shopifyId_to_shopifyGid_in_data_table.php b/src/migrations/m260617_100000_rename_shopifyId_to_shopifyGid_in_data_table.php new file mode 100644 index 00000000..3ba15894 --- /dev/null +++ b/src/migrations/m260617_100000_rename_shopifyId_to_shopifyGid_in_data_table.php @@ -0,0 +1,56 @@ +db->columnExists(Table::DATA, 'shopifyGid')) { + return true; + } + + $db = $this->getDb(); + $qb = $db->getQueryBuilder(); + + // Drop the existing index on shopifyId before renaming + $this->dropIndexIfExists(Table::DATA, ['shopifyId'], false); + + // Rename shopifyId → shopifyGid + $this->renameColumn(Table::DATA, 'shopifyId', 'shopifyGid'); + + // Recreate the index on the renamed column + $this->createIndex(null, Table::DATA, ['shopifyGid'], false); + + // Add generated shopifyId column — the numeric ID at the end of the GID + if ($db->getIsPgsql()) { + $expression = "regexp_replace(\"shopifyGid\", '^.*/', '')"; + } else { + $expression = "SUBSTRING_INDEX(`shopifyGid`, '/', -1)"; + } + + $this->execute("ALTER TABLE " . Table::DATA . " ADD COLUMN " . + $db->quoteColumnName('shopifyId') . ' ' . $qb->getColumnType($this->string()) . " GENERATED ALWAYS AS (" . + $expression . ") STORED;"); + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m260617_100000_rename_shopifyId_to_shopifyGid_in_data_table cannot be reverted.\n"; + return false; + } +} diff --git a/src/migrations/m260617_100001_rename_shopifyId_to_shopifyGid_in_bulk_operations_table.php b/src/migrations/m260617_100001_rename_shopifyId_to_shopifyGid_in_bulk_operations_table.php new file mode 100644 index 00000000..72b243f5 --- /dev/null +++ b/src/migrations/m260617_100001_rename_shopifyId_to_shopifyGid_in_bulk_operations_table.php @@ -0,0 +1,35 @@ +db->columnExists(Table::BULK_OPERATIONS, 'shopifyGid')) { + return true; + } + + $this->renameColumn(Table::BULK_OPERATIONS, 'shopifyId', 'shopifyGid'); + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m260617_100001_rename_shopifyId_to_shopifyGid_in_bulk_operations_table cannot be reverted.\n"; + return false; + } +} diff --git a/src/models/BulkOperation.php b/src/models/BulkOperation.php index 0e41d947..3bf4c1ca 100644 --- a/src/models/BulkOperation.php +++ b/src/models/BulkOperation.php @@ -26,9 +26,9 @@ class BulkOperation extends Model public ?int $id = null; /** - * @var string|null The Shopify ID of the bulk operation. + * @var string|null The Shopify GID of the bulk operation (e.g. "gid://shopify/BulkOperation/123456789"). */ - public ?string $shopifyId = null; + public ?string $shopifyGid = null; /** * @var string|null The URL of the bulk operation data. @@ -80,8 +80,8 @@ protected function defineRules(): array $rules = parent::defineRules(); $rules[] = [['id', 'objectCount'], 'number', 'integerOnly' => true]; - $rules[] = [['shopifyId', 'url'], 'string']; - $rules[] = [['id', 'shopifyId', 'url', 'objectCount', 'status', 'shopifyStatus', 'query', 'dateCreated', 'dateUpdated'], 'safe']; + $rules[] = [['shopifyGid', 'url'], 'string']; + $rules[] = [['id', 'shopifyGid', 'url', 'objectCount', 'status', 'shopifyStatus', 'query', 'dateCreated', 'dateUpdated'], 'safe']; return $rules; } diff --git a/src/models/Variant.php b/src/models/Variant.php index 688e4990..f07470f4 100644 --- a/src/models/Variant.php +++ b/src/models/Variant.php @@ -9,6 +9,7 @@ use craft\base\Model; use craft\helpers\Json; +use craft\shopify\helpers\Metafield as MetafieldHelper; use craft\shopify\Plugin; use DateTime; use yii\base\InvalidConfigException; @@ -16,6 +17,7 @@ /** * Variant model. * + * @property-read string $shopifyGid * @property-read string $shopifyId * @property-read string $title * @property-read string $sku @@ -31,7 +33,12 @@ class Variant extends Model public ?int $id = null; /** - * @var string|null The Shopify ID of the variant. + * @var string|null The Shopify GID of the variant (e.g. "gid://shopify/ProductVariant/123456789"). + */ + public ?string $shopifyGid = null; + + /** + * @var string|null The numeric Shopify ID of the variant (last segment of the GID). */ public ?string $shopifyId = null; @@ -103,7 +110,7 @@ protected function defineRules(): array { $rules = parent::defineRules(); - $rules[] = [['id', 'shopifyId', 'type', 'parentId', 'data', 'dateCreated', 'dateUpdated', 'uid'], 'safe']; + $rules[] = [['id', 'shopifyGid', 'shopifyId', 'type', 'parentId', 'data', 'dateCreated', 'dateUpdated', 'uid'], 'safe']; return $rules; } @@ -142,21 +149,21 @@ public function getData(): array } /** - * @param string|array $value + * @param string|array $value A list-shaped array of `{key, value}` objects, or a JSON-encoded string of the same. * @return void + * @throws \InvalidArgumentException if the value is not a list-shaped array or JSON string of one. */ public function setMetafields(string|array $value): void { if (is_string($value)) { $value = Json::decodeIfJson($value); - $value = collect($value)->mapWithKeys(function($d) { - return [ - $d['key'] => Json::decodeIfJson($d['value']), - ]; - }); } - $this->_metaFields = $value; + if (!is_array($value) || !array_is_list($value)) { + throw new \InvalidArgumentException('setMetafields() expects a list-shaped array of {key, value} objects or a JSON-encoded string of the same.'); + } + + $this->_metaFields = MetafieldHelper::normalizeToMap($value); } /** @@ -165,7 +172,7 @@ public function setMetafields(string|array $value): void */ public function getMetafields(): array { - if (!$this->shopifyId) { + if (!$this->shopifyGid) { return []; } @@ -173,16 +180,9 @@ public function getMetafields(): array return $this->_metaFields; } - $data = Plugin::getInstance()->getApi()->getShopifyDataByType('Metafield', $this->shopifyId); - - $metafields = $data - ->mapWithKeys(function($d) { - return [ - $d['key'] => Json::decodeIfJson($d['value']), - ]; - }); + $data = Plugin::getInstance()->getApi()->getShopifyDataByType('Metafield', $this->shopifyGid); - $this->setMetafields($metafields->all()); + $this->setMetafields($data->all()); return $this->_metaFields ?? []; } diff --git a/src/records/BulkOperation.php b/src/records/BulkOperation.php index 258649c8..0061e04c 100644 --- a/src/records/BulkOperation.php +++ b/src/records/BulkOperation.php @@ -17,7 +17,7 @@ * @since 6.0.0 * * @property int $id - * @property string $shopifyId + * @property string $shopifyGid * @property string $url * @property string $status * @property string $shopifyStatus diff --git a/src/records/Product.php b/src/records/Product.php index 36459106..46cc566a 100644 --- a/src/records/Product.php +++ b/src/records/Product.php @@ -39,6 +39,6 @@ public function getElement(): ActiveQueryInterface public function getData(): ActiveQueryInterface { - return $this->hasOne(ShopifyData::class, ['shopifyGid' => 'shopifyId']); + return $this->hasOne(ShopifyData::class, ['shopifyGid' => 'shopifyGid']); } } diff --git a/src/records/ShopifyData.php b/src/records/ShopifyData.php index 59625b0f..75da9387 100644 --- a/src/records/ShopifyData.php +++ b/src/records/ShopifyData.php @@ -17,6 +17,7 @@ * @since 6.0.0 * * @property int $id + * @property string $shopifyGid * @property string $shopifyId * @property string $type * @property string|array $data diff --git a/src/services/Api.php b/src/services/Api.php index 89f4b415..ec7cb293 100644 --- a/src/services/Api.php +++ b/src/services/Api.php @@ -176,7 +176,7 @@ public function getShop(bool $update = false): ?array $shopRecord = new ShopifyData(); } - $shopRecord->shopifyId = $response['id']; + $shopRecord->shopifyGid = $response['id']; $shopRecord->type = 'Shop'; $shopRecord->data = $response; diff --git a/src/services/BulkOperations.php b/src/services/BulkOperations.php index 8e6cb586..cdb9e1db 100644 --- a/src/services/BulkOperations.php +++ b/src/services/BulkOperations.php @@ -66,14 +66,26 @@ public function getAllBulkOperations(): Collection return collect($bulkOps); } + /** + * @param string $gid + * @return BulkOperation|null + * @throws InvalidConfigException + * @since 8.0.0 + */ + public function getBulkOperationByShopifyGid(string $gid): ?BulkOperation + { + return $this->getAllBulkOperations()->firstWhere('shopifyGid', $gid); + } + /** * @param string $shopifyId * @return BulkOperation|null * @throws InvalidConfigException + * @deprecated in 8.0.0. Use [[getBulkOperationByShopifyGid()]] instead. */ public function getBulkOperationByShopifyId(string $shopifyId): ?BulkOperation { - return $this->getAllBulkOperations()->firstWhere('shopifyId', $shopifyId); + return $this->getBulkOperationByShopifyGid($shopifyId); } /** @@ -211,7 +223,7 @@ public function nextBulkOperation(): bool } $bulkOperation->shopifyStatus = $op['status']; - $bulkOperation->shopifyId = $op['id']; + $bulkOperation->shopifyGid = $op['id']; $this->saveBulkOperation($bulkOperation); return true; @@ -237,7 +249,7 @@ public function handleBulkOperationFinished(array $payload): void } // Load our local record of the bulk op: - $bulkOperation = $this->getBulkOperationByShopifyId($payload['admin_graphql_api_id']); + $bulkOperation = $this->getBulkOperationByShopifyGid($payload['admin_graphql_api_id']); if (!$bulkOperation) { // Ok... maybe it was initiated for a different environment? @@ -321,7 +333,7 @@ public function queueNextBulkOperation(): bool /** @var BulkOperation $bulkOperation */ $bulkOperation = Craft::createObject(array_merge($nextToProcess, ['class' => BulkOperation::class])); if (!Queue::push(new ProcessBulkOperationData([ - 'bulkOperationShopifyId' => $bulkOperation->shopifyId, + 'bulkOperationShopifyGid' => $bulkOperation->shopifyGid, 'dataUrl' => $bulkOperation->url, 'objectCount' => $bulkOperation->objectCount, 'clearData' => $bulkOperation->clearData, @@ -351,7 +363,7 @@ public function saveBulkOperation(BulkOperation $bulkOperation, bool $runValidat if ($bulkOperation->id) { $record = BulkOperationRecord::findOne($bulkOperation->id); } else { - $record = BulkOperationRecord::findOne(['shopifyId' => $bulkOperation->shopifyId]); + $record = BulkOperationRecord::findOne(['shopifyGid' => $bulkOperation->shopifyGid]); } if (!$record) { @@ -365,7 +377,7 @@ public function saveBulkOperation(BulkOperation $bulkOperation, bool $runValidat } $record->clearData = $bulkOperation->clearData; - $record->shopifyId = $bulkOperation->shopifyId; + $record->shopifyGid = $bulkOperation->shopifyGid; $record->url = $bulkOperation->url; $record->objectCount = $bulkOperation->objectCount; $record->query = $bulkOperation->query; @@ -445,7 +457,7 @@ private function _createBulkOperationQuery(): Query 'id', 'objectCount', 'query', - 'shopifyId', + 'shopifyGid', 'status', 'shopifyStatus', 'url', diff --git a/src/services/Products.php b/src/services/Products.php index 52483321..f89e2a53 100644 --- a/src/services/Products.php +++ b/src/services/Products.php @@ -8,12 +8,10 @@ use craft\events\ConfigEvent; use craft\helpers\ArrayHelper; use craft\helpers\Db; -use craft\helpers\Json; use craft\helpers\ProjectConfig; use craft\helpers\StringHelper; use craft\models\FieldLayout; use craft\shopify\collections\VariantCollection; -use craft\shopify\db\Table; use craft\shopify\elements\Product; use craft\shopify\events\ShopifyProductSyncEvent; use craft\shopify\models\Variant; @@ -58,16 +56,29 @@ class Products extends Component */ public const EVENT_BEFORE_SYNCHRONIZE_PRODUCT = 'beforeSynchronizeProduct'; + /** + * @param string $gid + * @return void + * @throws InvalidConfigException + * @throws \yii\db\Exception + * @since 8.0.0 + */ + public function syncProductByShopifyGid(string $gid): void + { + $gid = $this->normalizeShopifyGid($gid); + Plugin::getInstance()->getBulkOperations()->createBulkOperation((string)Plugin::getInstance()->getApi()->getProductGql($gid), $gid); + } + /** * @param string $id * @return void * @throws InvalidConfigException * @throws \yii\db\Exception + * @deprecated in 8.0.0. Use [[syncProductByShopifyGid()]] instead. */ public function syncProductByShopifyId(string $id): void { - $shopifyId = $this->normalizeShopifyGid($id); - Plugin::getInstance()->getBulkOperations()->createBulkOperation((string)Plugin::getInstance()->getApi()->getProductGql($id), $shopifyId); + $this->syncProductByShopifyGid($id); } /** @@ -100,7 +111,7 @@ public function syncProductByInventoryItemId($id): void $productId = $item['variant']['product']['id']; - $this->syncProductByShopifyId($productId); + $this->syncProductByShopifyGid($productId); } /** @@ -178,52 +189,72 @@ public function normalizeShopifyGid(string $shopifyId, string $type = 'Product') } /** - * Deletes a product element by the Shopify ID. + * Deletes a product element by the Shopify GID. * + * @param string $gid + * @return bool Whether the deletion was performed. Returns false if `$gid` is empty. + * @throws \Throwable + * @throws StaleObjectException + * @since 8.0.0 + */ + public function deleteProductByShopifyGid(string $gid): bool + { + if (!$gid) { + return false; + } + + $gid = $this->normalizeShopifyGid($gid); + + if ($product = Product::find()->shopifyGid($gid)->one()) { + // We hard delete because it will have been hard deleted in Shopify + Craft::$app->getElements()->deleteElement($product, true); + } + + $this->deleteShopifyDataByShopifyGid($gid); + + return true; + } + + /** * @param $id * @return void * @throws \Throwable * @throws StaleObjectException + * @deprecated in 8.0.0. Use [[deleteProductByShopifyGid()]] instead. */ public function deleteProductByShopifyId($id): void { - if ($id) { - if ($product = Product::find()->shopifyId($id)->one()) { - // We hard delete because it will have been hard deleted in Shopify - Craft::$app->getElements()->deleteElement($product, true); - } - - // Delete data in shopify data table - // Delete the product data - $shopifyId = $this->normalizeShopifyGid($id); - $this->deleteShopifyDataByShopifyId($shopifyId); + if (!$id) { + return; } + + $this->deleteProductByShopifyGid($id); } /** - * @param string $shopifyId + * @param string $gid * @return void * @throws StaleObjectException * @throws \Throwable - * @since 6.0.0 + * @since 8.0.0 */ - public function deleteShopifyDataByShopifyId(string $shopifyId): void + public function deleteShopifyDataByShopifyGid(string $gid): void { - // Support both id and gid - $shopifyId = $this->normalizeShopifyGid($shopifyId); + // Support both numeric ID and GID + $gid = $this->normalizeShopifyGid($gid); /** @var ShopifyData|null $shopifyData */ - $shopifyData = ShopifyData::find()->where(['shopifyId' => $shopifyId])->one(); + $shopifyData = ShopifyData::find()->where(['shopifyGid' => $gid])->one(); // Delete if possible $shopifyData?->delete(); // Delete any child data that may still exist /** @var ShopifyData[] $shopifyData */ - $shopifyData = ShopifyData::find()->where(['parentId' => $shopifyId])->all(); + $shopifyData = ShopifyData::find()->where(['parentId' => $gid])->all(); $childIds = []; foreach ($shopifyData as $data) { - $childIds[] = $data->shopifyId; + $childIds[] = $data->shopifyGid; $data->delete(); } @@ -233,12 +264,24 @@ public function deleteShopifyDataByShopifyId(string $shopifyId): void /** @var ShopifyData[] $shopifyData */ $shopifyData = ShopifyData::find()->where(['parentId' => $childId])->all(); foreach ($shopifyData as $data) { - $childIds[] = $data->shopifyId; + $childIds[] = $data->shopifyGid; $data->delete(); } } } + /** + * @param string $shopifyId + * @return void + * @throws StaleObjectException + * @throws \Throwable + * @deprecated in 8.0.0. Use [[deleteShopifyDataByShopifyGid()]] instead. + */ + public function deleteShopifyDataByShopifyId(string $shopifyId): void + { + $this->deleteShopifyDataByShopifyGid($shopifyId); + } + /** * @param array|Product[] $products * @return array @@ -247,23 +290,7 @@ public function deleteShopifyDataByShopifyId(string $shopifyId): void public function eagerLoadMetafieldsForProducts(array $products): array { return $this->_eagerLoadTypeOnProducts($products, 'Metafield', function($product, $rows) { - $metafields = collect($rows) - ->mapWithKeys(function($d, $key) { - /** @var ShopifyData $d */ - $data = Json::decodeIfJson($d->data); - - // Map if the data has `key` and `value` properties - if (!is_array($data) || !isset($data['key']) || !isset($data['value'])) { - return []; - } - - return [ - $data['key'] => Json::decodeIfJson($data['value']), - ]; - }) - ->all(); - - $product->setMetafields($metafields); + $product->setMetafields($rows); }); } @@ -290,7 +317,7 @@ public function eagerLoadVariantsForProducts(array $products): array $variantsByProductId = []; $return = $this->_eagerLoadTypeOnProducts($products, 'ProductVariant', function($product, $rows) use (&$variantsByProductId, &$variantIds) { foreach ($rows as $row) { - $variantIds[] = $row->shopifyId; + $variantIds[] = $row->shopifyGid; } $variantsByProductId[$product->shopifyGid] = $rows; @@ -309,18 +336,10 @@ public function eagerLoadVariantsForProducts(array $products): array $variants = VariantCollection::make($variantsByProductId[$product->shopifyGid]); if ($metafieldsData->isNotEmpty()) { - $variants->map(function(Variant$variant) use ($metafieldsData) { - $metafields = $metafieldsData->get($variant->shopifyId); + $variants->map(function(Variant $variant) use ($metafieldsData) { + $metafields = $metafieldsData->get($variant->shopifyGid); if (!empty($metafields)) { - $variant->setMetafields(collect($metafields)->mapWithKeys(function($d) { - $data = Json::decodeIfJson($d->data); - if (!is_array($data) || !isset($data['key']) || !isset($data['value'])) { - return []; - } - return [ - $data['key'] => Json::decodeIfJson($data['value']), - ]; - })->all()); + $variant->setMetafields($metafields->all()); } }); } diff --git a/tests/fixtures/BulkOperationsFixture.php b/tests/fixtures/BulkOperationsFixture.php index 1b4fd2f9..486b52ac 100644 --- a/tests/fixtures/BulkOperationsFixture.php +++ b/tests/fixtures/BulkOperationsFixture.php @@ -26,7 +26,7 @@ public function load(): void foreach ($rows as $key => $row) { $model = new BulkOperation(); - $model->shopifyId = $row['shopifyId']; + $model->shopifyGid = $row['shopifyGid']; $model->url = $row['url']; $model->objectCount = (int)($row['objectCount'] ?? 0); $model->query = $row['query'] ?? null; diff --git a/tests/fixtures/ShopifyDataFixture.php b/tests/fixtures/ShopifyDataFixture.php index 62144fbe..db8892cb 100644 --- a/tests/fixtures/ShopifyDataFixture.php +++ b/tests/fixtures/ShopifyDataFixture.php @@ -29,7 +29,7 @@ public function load(): void foreach ($rows as $row) { $uid = $row['uid']; \Yii::$app->db->createCommand()->insert(Table::DATA, [ - 'shopifyId' => $row['shopifyId'], + 'shopifyGid' => $row['shopifyGid'], 'type' => $row['type'], 'data' => is_array($row['data']) ? json_encode($row['data']) : $row['data'], 'parentId' => $row['parentId'], diff --git a/tests/fixtures/data/shopify-bulk-operations.php b/tests/fixtures/data/shopify-bulk-operations.php index d3774db8..4725c0d8 100644 --- a/tests/fixtures/data/shopify-bulk-operations.php +++ b/tests/fixtures/data/shopify-bulk-operations.php @@ -3,7 +3,7 @@ return [ 'op_completed_74' => [ 'id' => 74, - 'shopifyId' => 'gid://shopify/BulkOperation/4848685842483', + 'shopifyGid' => 'gid://shopify/BulkOperation/4848685842483', 'url' => 'https://storage.googleapis.com/shopify-tiers-assets-prod-us-east1/bulk-operation-outputs/45bd3bca5ff2b0445c4ee86c7b6a?GoogleAccessId=assets-us-prod%40shopify-tiers.iam.gserviceaccount.com&Expires=1781775894&Signature=gYorZwhi9arbpjvRo5p%2BqkRn9NYhDNwmi6ZJ%2FHSH3BgiQqu62pByOOUpZ%2BG3Qe4QI%2Frc%2F8XJYoSudrjrRBtnqn%2Fess73avZYDa7%2BmG4qPyJ%2BToJu04H5xYo%2FiIbSj6mJsHJJ1td2w%2B0O3hbbcPmVFaEZkR8vZpfa0MkC8aPO4VFbj%2FfnJdTDr5kmLV5E6PdlFy8vfKCBsXoj4Hh0dDImi9IFSEjEx80hc7%2FXtxBe374vam%2BM%2BGY0KCFglfNmnWyPjGJtnjXciRprOkCJ%2Fcz5vlNbYiR%2FAdRKF6i51HeztiBmD8vCrstiktNug9wruwYcawmGqQiXa4oJMLAWKOe4lg%3D%3D&response-content-disposition=attachment%3B+filename%3D%22staging-bulk-4848685842483.jsonl%22%3B+filename%2A%3DUTF-8%27%27staging-bulk-4848685842483.jsonl&response-content-type=application%2Fjsonl', 'objectCount' => 60, 'query' => 'query { products(query: "id:6656149192755") { edges { node { descriptionHtml createdAt handle id media { edges { node { mediaContentType alt id ... on MediaImage { createdAt updatedAt image { altText height width url } } } } } metafields { edges { node { id key value } } } options { id name position values optionValues { id name hasVariants } } productType publishedAt status tags templateSuffix title totalInventory updatedAt variants { edges { node { usContextualPricing:contextualPricing(context:{country:US}) { price { amount currencyCode } compareAtPrice { amount currencyCode } } id barcode compareAtPrice createdAt displayName price sku taxable title updatedAt position image { altText height id url width originalSrc src transformedSrc } inventoryItem { id countryCodeOfOrigin createdAt updatedAt sku tracked unitCost { amount currencyCode } } inventoryPolicy inventoryQuantity metafields { edges { node { id key value } } } product { id } selectedOptions { name value } } } } vendor } } } }', @@ -16,7 +16,7 @@ ], 'op_completed_73' => [ 'id' => 73, - 'shopifyId' => 'gid://shopify/BulkOperation/4846181023795', + 'shopifyGid' => 'gid://shopify/BulkOperation/4846181023795', 'url' => 'https://storage.googleapis.com/shopify-tiers-assets-prod-us-east1/bulk-operation-outputs/0b38d4b425e19b4600ddc7d6c366?GoogleAccessId=assets-us-prod%40shopify-tiers.iam.gserviceaccount.com&Expires=1781705438&Signature=PMTzLXL%2B7Nb%2B5RCPXypekUy8BmX1AZTfqIxT0TkwKe7%2FJ4T5HRCuZySB4gZN5wCq%2Fe39DKtGFKuJwscJOoWp4tSKGCnPNtZECvzNnZKo8rDZurffaTFOWsHIgJ2LqlMYF%2BPm675YY0Lh%2FfWDcp5LgWBGCgxRHwGa2YcAyRxE8jJBFC4b8wXmJEQ8bnEKOB5AQpeVK5p%2B7PF5kSXSMp%2F83KAUETJUnY2KtlczABAo8JcyoHBVj7n6pZhVHal%2Bz1HYdC8rZZzO%2FsA3iJ2mX7z%2FrUFaZAoGIrazPJXninpLyA6wz%2F4y41HDV390ekMj6va4tu%2BIA1VuZo8MN4FHnelnSQ%3D%3D&response-content-disposition=attachment%3B+filename%3D%22staging-bulk-4846181023795.jsonl%22%3B+filename%2A%3DUTF-8%27%27staging-bulk-4846181023795.jsonl&response-content-type=application%2Fjsonl', 'objectCount' => 3, 'query' => 'query { products(query: "id:7136093208627") { edges { node { descriptionHtml createdAt handle id media { edges { node { mediaContentType alt id ... on MediaImage { createdAt updatedAt image { altText height width url } } } } } metafields { edges { node { id key value } } } options { id name position values optionValues { id name hasVariants } } productType publishedAt status tags templateSuffix title totalInventory updatedAt variants { edges { node { usContextualPricing:contextualPricing(context:{country:US}) { price { amount currencyCode } compareAtPrice { amount currencyCode } } id barcode compareAtPrice createdAt displayName price sku taxable title updatedAt position image { altText height id url width originalSrc src transformedSrc } inventoryItem { id countryCodeOfOrigin createdAt updatedAt sku tracked unitCost { amount currencyCode } } inventoryPolicy inventoryQuantity metafields { edges { node { id key value } } } product { id } selectedOptions { name value } } } } vendor translations_es: translations(locale:"es") { key value } } } } }', @@ -29,7 +29,7 @@ ], 'op_completed_72' => [ 'id' => 72, - 'shopifyId' => 'gid://shopify/BulkOperation/4846126170163', + 'shopifyGid' => 'gid://shopify/BulkOperation/4846126170163', 'url' => 'https://storage.googleapis.com/shopify-tiers-assets-prod-us-east1/bulk-operation-outputs/1261ad672da99ee3b988a510cef2?GoogleAccessId=assets-us-prod%40shopify-tiers.iam.gserviceaccount.com&Expires=1781703757&Signature=anJScMX3XZ4HGBwJbEnGsyQElHcJG%2BplwGMocdYEL%2FTJoiF13cSQAV6CKKiQ7hfzlrkuHt1QQAZl68TX38SJLtvVBXE64hA2C7eLAb9ncpJvBCcXo54TjCGkyns54dBYjsmNtOXdHBuIbAsjbFQ52Elw3QZQV57yjITmqEgDkHdqpgb011C0bxX4lpwMW4e%2FpV0JgITFeYJrfRNcFEgu6RzsWQvuj8114VSvqgHGN2k50AVyUtvY9b39wsvZTNOP9%2FNqdary%2BO0RPY7NqmQUF%2BHD6cBm%2F2EF%2F%2B0RsfpdUJ3loWUtXAuOH9kIZP2DZFnoxoAYu6QGjSNLP01V%2F3hvSA%3D%3D&response-content-disposition=attachment%3B+filename%3D%22staging-bulk-4846126170163.jsonl%22%3B+filename%2A%3DUTF-8%27%27staging-bulk-4846126170163.jsonl&response-content-type=application%2Fjsonl', 'objectCount' => 3, 'query' => 'query { products(query: "id:7136068075571") { edges { node { descriptionHtml createdAt handle id media { edges { node { mediaContentType alt id ... on MediaImage { createdAt updatedAt image { altText height width url } } } } } metafields { edges { node { id key value } } } options { id name position values optionValues { id name hasVariants } } productType publishedAt status tags templateSuffix title totalInventory updatedAt variants { edges { node { usContextualPricing:contextualPricing(context:{country:US}) { price { amount currencyCode } compareAtPrice { amount currencyCode } } id barcode compareAtPrice createdAt displayName price sku taxable title updatedAt position image { altText height id url width originalSrc src transformedSrc } inventoryItem { id countryCodeOfOrigin createdAt updatedAt sku tracked unitCost { amount currencyCode } } inventoryPolicy inventoryQuantity metafields { edges { node { id key value } } } product { id } selectedOptions { name value } } } } vendor translations_es: translations(locale:"es") { key value } } } } }', @@ -42,7 +42,7 @@ ], 'op_completed_71' => [ 'id' => 71, - 'shopifyId' => 'gid://shopify/BulkOperation/4846010433587', + 'shopifyGid' => 'gid://shopify/BulkOperation/4846010433587', 'url' => 'https://storage.googleapis.com/shopify-tiers-assets-prod-us-east1/bulk-operation-outputs/6a97ef72dca29699d67ba70d3e8b?GoogleAccessId=assets-us-prod%40shopify-tiers.iam.gserviceaccount.com&Expires=1781700339&Signature=mfZ76fGjrnk9G04sllkiktezTLFsLoikUhqftMHP4CNt5fVgaGIQY1%2Buvh2lVKLXGlIH%2F44WXiAZD%2F0gK28gRnT0kBLcENPR3ZmoAmpFHDGbB4MKEv6bUkrJv7wlxZD0ZilyYgnVLEyNuV3CvLgqe9HRz3S8bfeIH3jfxt5qxCzfcdtNIoRg0qmkageYF6drLCdCQ7y7UPvCU9e0De9HEuiaycfFty6Gyc2%2BO1pFCxbQu0I6hZJbSbney3QJvdatNo8j05zWFFalPI9iPXUZnHxGm6Sc4ISP2FZCcutcJ9r617%2F5uSYSsHi%2F%2F5e0Deb%2FHAEQJYiZJk8YXHL7tvNqHQ%3D%3D&response-content-disposition=attachment%3B+filename%3D%22staging-bulk-4846010433587.jsonl%22%3B+filename%2A%3DUTF-8%27%27staging-bulk-4846010433587.jsonl&response-content-type=application%2Fjsonl', 'objectCount' => 3, 'query' => 'query { products(query: "id:7136099729459") { edges { node { descriptionHtml createdAt handle id media { edges { node { mediaContentType alt id ... on MediaImage { createdAt updatedAt image { altText height width url } } } } } metafields { edges { node { id key value } } } options { id name position values optionValues { id name hasVariants } } productType publishedAt status tags templateSuffix title totalInventory updatedAt variants { edges { node { usContextualPricing:contextualPricing(context:{country:US}) { price { amount currencyCode } compareAtPrice { amount currencyCode } } id barcode compareAtPrice createdAt displayName price sku taxable title updatedAt position image { altText height id url width originalSrc src transformedSrc } inventoryItem { id countryCodeOfOrigin createdAt updatedAt sku tracked unitCost { amount currencyCode } } inventoryPolicy inventoryQuantity metafields { edges { node { id key value } } } product { id } selectedOptions { name value } } } } vendor translations_es: translations(locale:"es") { key value } } } } }', @@ -55,7 +55,7 @@ ], 'op_completed_70' => [ 'id' => 70, - 'shopifyId' => 'gid://shopify/BulkOperation/4845996933171', + 'shopifyGid' => 'gid://shopify/BulkOperation/4845996933171', 'url' => 'https://storage.googleapis.com/shopify-tiers-assets-prod-us-east1/bulk-operation-outputs/45e4ebfb3778f9f27c5709136189?GoogleAccessId=assets-us-prod%40shopify-tiers.iam.gserviceaccount.com&Expires=1781700017&Signature=XOm1n1r681pq%2B7iQpcLHD%2FMmYOqv8Fa33LOcE8LSdYLezdRDBR6wiYZ2jCNvtA5nOGBzUe3XDadhTbaxYVqlIaeZDx1HBB14wPfHjbLmNU5hRXa0094tQiw9IU23iCi4lM%2Bc2PX5SrC5WJbFwcDrl%2BiOzrazNpjgp5rVnW4QWPepNaN7TBeiHMHBqxNnY7FxCEi2FQxK6cchM1Rjuro0QmtNufRxiuw9vhxbjf8b6VOtZx4agdnNms3Rhjioq7191qfdeLvzaz2aepXoJd%2BhAVAmJo5wgMkgRF2wddgCtrYe6SHFdxP%2BCrlV1XHpbOQiI1G52%2BQorUps3EOlX8rHfg%3D%3D&response-content-disposition=attachment%3B+filename%3D%22staging-bulk-4845996933171.jsonl%22%3B+filename%2A%3DUTF-8%27%27staging-bulk-4845996933171.jsonl&response-content-type=application%2Fjsonl', 'objectCount' => 1880, 'query' => 'query { products { edges { node { descriptionHtml createdAt handle id media { edges { node { mediaContentType alt id ... on MediaImage { createdAt updatedAt image { altText height width url } } } } } metafields { edges { node { id key value } } } options { id name position values optionValues { id name hasVariants } } productType publishedAt status tags templateSuffix title totalInventory updatedAt variants { edges { node { usContextualPricing:contextualPricing(context:{country:US}) { price { amount currencyCode } compareAtPrice { amount currencyCode } } id barcode compareAtPrice createdAt displayName price sku taxable title updatedAt position image { altText height id url width originalSrc src transformedSrc } inventoryItem { id countryCodeOfOrigin createdAt updatedAt sku tracked unitCost { amount currencyCode } } inventoryPolicy inventoryQuantity metafields { edges { node { id key value } } } product { id } selectedOptions { name value } } } } vendor translations_es: translations(locale:"es") { key value } } } } }', diff --git a/tests/fixtures/data/shopify-data.php b/tests/fixtures/data/shopify-data.php index 183d5813..7b1aca5d 100644 --- a/tests/fixtures/data/shopify-data.php +++ b/tests/fixtures/data/shopify-data.php @@ -3,7 +3,7 @@ return [ 'Product_product_7136060145715' => [ 'id' => 64533, - 'shopifyId' => 'gid://shopify/Product/7136060145715', + 'shopifyGid' => 'gid://shopify/Product/7136060145715', 'type' => 'Product', 'data' => [ 'id' => 'gid://shopify/Product/7136060145715', @@ -126,7 +126,7 @@ ], 'Product_product_7136060964915' => [ 'id' => 64581, - 'shopifyId' => 'gid://shopify/Product/7136060964915', + 'shopifyGid' => 'gid://shopify/Product/7136060964915', 'type' => 'Product', 'data' => [ 'id' => 'gid://shopify/Product/7136060964915', @@ -262,7 +262,7 @@ ], 'Product_product_7136062865459' => [ 'id' => 64674, - 'shopifyId' => 'gid://shopify/Product/7136062865459', + 'shopifyGid' => 'gid://shopify/Product/7136062865459', 'type' => 'Product', 'data' => [ 'id' => 'gid://shopify/Product/7136062865459', @@ -336,7 +336,7 @@ ], 'Product_product_7136074399795' => [ 'id' => 65044, - 'shopifyId' => 'gid://shopify/Product/7136074399795', + 'shopifyGid' => 'gid://shopify/Product/7136074399795', 'type' => 'Product', 'data' => [ 'id' => 'gid://shopify/Product/7136074399795', @@ -396,7 +396,7 @@ ], 'Product_product_7136075251763' => [ 'id' => 65080, - 'shopifyId' => 'gid://shopify/Product/7136075251763', + 'shopifyGid' => 'gid://shopify/Product/7136075251763', 'type' => 'Product', 'data' => [ 'id' => 'gid://shopify/Product/7136075251763', @@ -456,7 +456,7 @@ ], 'Product_product_7136076070963' => [ 'id' => 65107, - 'shopifyId' => 'gid://shopify/Product/7136076070963', + 'shopifyGid' => 'gid://shopify/Product/7136076070963', 'type' => 'Product', 'data' => [ 'id' => 'gid://shopify/Product/7136076070963', @@ -515,7 +515,7 @@ ], 'Product_product_7136089669683' => [ 'id' => 65620, - 'shopifyId' => 'gid://shopify/Product/7136089669683', + 'shopifyGid' => 'gid://shopify/Product/7136089669683', 'type' => 'Product', 'data' => [ 'id' => 'gid://shopify/Product/7136089669683', @@ -574,7 +574,7 @@ ], 'Product_product_7136090816563' => [ 'id' => 65644, - 'shopifyId' => 'gid://shopify/Product/7136090816563', + 'shopifyGid' => 'gid://shopify/Product/7136090816563', 'type' => 'Product', 'data' => [ 'id' => 'gid://shopify/Product/7136090816563', @@ -633,7 +633,7 @@ ], 'Product_product_7136093863987' => [ 'id' => 65773, - 'shopifyId' => 'gid://shopify/Product/7136093863987', + 'shopifyGid' => 'gid://shopify/Product/7136093863987', 'type' => 'Product', 'data' => [ 'id' => 'gid://shopify/Product/7136093863987', @@ -688,7 +688,7 @@ ], 'Product_product_7136099500083' => [ 'id' => 66025, - 'shopifyId' => 'gid://shopify/Product/7136099500083', + 'shopifyGid' => 'gid://shopify/Product/7136099500083', 'type' => 'Product', 'data' => [ 'id' => 'gid://shopify/Product/7136099500083', @@ -749,7 +749,7 @@ ], 'MediaImage_mediaimage_23117943373875' => [ 'id' => 64534, - 'shopifyId' => 'gid://shopify/MediaImage/23117943373875', + 'shopifyGid' => 'gid://shopify/MediaImage/23117943373875', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117943373875', @@ -772,7 +772,7 @@ ], 'MediaImage_mediaimage_23117943406643' => [ 'id' => 64535, - 'shopifyId' => 'gid://shopify/MediaImage/23117943406643', + 'shopifyGid' => 'gid://shopify/MediaImage/23117943406643', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117943406643', @@ -795,7 +795,7 @@ ], 'MediaImage_mediaimage_23117943439411' => [ 'id' => 64536, - 'shopifyId' => 'gid://shopify/MediaImage/23117943439411', + 'shopifyGid' => 'gid://shopify/MediaImage/23117943439411', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117943439411', @@ -818,7 +818,7 @@ ], 'MediaImage_mediaimage_23117943472179' => [ 'id' => 64537, - 'shopifyId' => 'gid://shopify/MediaImage/23117943472179', + 'shopifyGid' => 'gid://shopify/MediaImage/23117943472179', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117943472179', @@ -841,7 +841,7 @@ ], 'MediaImage_mediaimage_23117943504947' => [ 'id' => 64538, - 'shopifyId' => 'gid://shopify/MediaImage/23117943504947', + 'shopifyGid' => 'gid://shopify/MediaImage/23117943504947', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117943504947', @@ -864,7 +864,7 @@ ], 'ProductVariant_productvariant_41966390083635' => [ 'id' => 64539, - 'shopifyId' => 'gid://shopify/ProductVariant/41966390083635', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966390083635', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966390083635', @@ -919,7 +919,7 @@ ], 'ProductVariant_productvariant_41966390116403' => [ 'id' => 64540, - 'shopifyId' => 'gid://shopify/ProductVariant/41966390116403', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966390116403', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966390116403', @@ -974,7 +974,7 @@ ], 'ProductVariant_productvariant_41966390149171' => [ 'id' => 64541, - 'shopifyId' => 'gid://shopify/ProductVariant/41966390149171', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966390149171', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966390149171', @@ -1029,7 +1029,7 @@ ], 'ProductVariant_productvariant_41966390181939' => [ 'id' => 64542, - 'shopifyId' => 'gid://shopify/ProductVariant/41966390181939', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966390181939', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966390181939', @@ -1084,7 +1084,7 @@ ], 'ProductVariant_productvariant_41966390214707' => [ 'id' => 64543, - 'shopifyId' => 'gid://shopify/ProductVariant/41966390214707', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966390214707', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966390214707', @@ -1139,7 +1139,7 @@ ], 'ProductVariant_productvariant_41966390247475' => [ 'id' => 64544, - 'shopifyId' => 'gid://shopify/ProductVariant/41966390247475', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966390247475', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966390247475', @@ -1194,7 +1194,7 @@ ], 'ProductVariant_productvariant_41966390280243' => [ 'id' => 64545, - 'shopifyId' => 'gid://shopify/ProductVariant/41966390280243', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966390280243', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966390280243', @@ -1249,7 +1249,7 @@ ], 'ProductVariant_productvariant_41966390313011' => [ 'id' => 64546, - 'shopifyId' => 'gid://shopify/ProductVariant/41966390313011', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966390313011', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966390313011', @@ -1304,7 +1304,7 @@ ], 'ProductVariant_productvariant_41966390345779' => [ 'id' => 64547, - 'shopifyId' => 'gid://shopify/ProductVariant/41966390345779', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966390345779', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966390345779', @@ -1359,7 +1359,7 @@ ], 'MediaImage_mediaimage_23117946912819' => [ 'id' => 64582, - 'shopifyId' => 'gid://shopify/MediaImage/23117946912819', + 'shopifyGid' => 'gid://shopify/MediaImage/23117946912819', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117946912819', @@ -1382,7 +1382,7 @@ ], 'MediaImage_mediaimage_23117946945587' => [ 'id' => 64583, - 'shopifyId' => 'gid://shopify/MediaImage/23117946945587', + 'shopifyGid' => 'gid://shopify/MediaImage/23117946945587', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117946945587', @@ -1405,7 +1405,7 @@ ], 'MediaImage_mediaimage_23117946978355' => [ 'id' => 64584, - 'shopifyId' => 'gid://shopify/MediaImage/23117946978355', + 'shopifyGid' => 'gid://shopify/MediaImage/23117946978355', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117946978355', @@ -1428,7 +1428,7 @@ ], 'MediaImage_mediaimage_23117947011123' => [ 'id' => 64585, - 'shopifyId' => 'gid://shopify/MediaImage/23117947011123', + 'shopifyGid' => 'gid://shopify/MediaImage/23117947011123', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117947011123', @@ -1451,7 +1451,7 @@ ], 'MediaImage_mediaimage_23117947043891' => [ 'id' => 64586, - 'shopifyId' => 'gid://shopify/MediaImage/23117947043891', + 'shopifyGid' => 'gid://shopify/MediaImage/23117947043891', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117947043891', @@ -1474,7 +1474,7 @@ ], 'ProductVariant_productvariant_41966393098291' => [ 'id' => 64587, - 'shopifyId' => 'gid://shopify/ProductVariant/41966393098291', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966393098291', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966393098291', @@ -1529,7 +1529,7 @@ ], 'ProductVariant_productvariant_41966393131059' => [ 'id' => 64588, - 'shopifyId' => 'gid://shopify/ProductVariant/41966393131059', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966393131059', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966393131059', @@ -1584,7 +1584,7 @@ ], 'ProductVariant_productvariant_41966393163827' => [ 'id' => 64589, - 'shopifyId' => 'gid://shopify/ProductVariant/41966393163827', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966393163827', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966393163827', @@ -1639,7 +1639,7 @@ ], 'ProductVariant_productvariant_41966393196595' => [ 'id' => 64590, - 'shopifyId' => 'gid://shopify/ProductVariant/41966393196595', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966393196595', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966393196595', @@ -1694,7 +1694,7 @@ ], 'ProductVariant_productvariant_41966393229363' => [ 'id' => 64591, - 'shopifyId' => 'gid://shopify/ProductVariant/41966393229363', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966393229363', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966393229363', @@ -1749,7 +1749,7 @@ ], 'ProductVariant_productvariant_41966393262131' => [ 'id' => 64592, - 'shopifyId' => 'gid://shopify/ProductVariant/41966393262131', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966393262131', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966393262131', @@ -1804,7 +1804,7 @@ ], 'ProductVariant_productvariant_41966393294899' => [ 'id' => 64593, - 'shopifyId' => 'gid://shopify/ProductVariant/41966393294899', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966393294899', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966393294899', @@ -1859,7 +1859,7 @@ ], 'ProductVariant_productvariant_41966393327667' => [ 'id' => 64594, - 'shopifyId' => 'gid://shopify/ProductVariant/41966393327667', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966393327667', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966393327667', @@ -1914,7 +1914,7 @@ ], 'ProductVariant_productvariant_41966393360435' => [ 'id' => 64595, - 'shopifyId' => 'gid://shopify/ProductVariant/41966393360435', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966393360435', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966393360435', @@ -1969,7 +1969,7 @@ ], 'ProductVariant_productvariant_41966393393203' => [ 'id' => 64596, - 'shopifyId' => 'gid://shopify/ProductVariant/41966393393203', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966393393203', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966393393203', @@ -2024,7 +2024,7 @@ ], 'ProductVariant_productvariant_41966393425971' => [ 'id' => 64597, - 'shopifyId' => 'gid://shopify/ProductVariant/41966393425971', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966393425971', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966393425971', @@ -2079,7 +2079,7 @@ ], 'MediaImage_mediaimage_23117955760179' => [ 'id' => 64675, - 'shopifyId' => 'gid://shopify/MediaImage/23117955760179', + 'shopifyGid' => 'gid://shopify/MediaImage/23117955760179', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117955760179', @@ -2102,7 +2102,7 @@ ], 'MediaImage_mediaimage_23117955825715' => [ 'id' => 64676, - 'shopifyId' => 'gid://shopify/MediaImage/23117955825715', + 'shopifyGid' => 'gid://shopify/MediaImage/23117955825715', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117955825715', @@ -2125,7 +2125,7 @@ ], 'MediaImage_mediaimage_23117955858483' => [ 'id' => 64677, - 'shopifyId' => 'gid://shopify/MediaImage/23117955858483', + 'shopifyGid' => 'gid://shopify/MediaImage/23117955858483', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117955858483', @@ -2148,7 +2148,7 @@ ], 'MediaImage_mediaimage_23117955891251' => [ 'id' => 64678, - 'shopifyId' => 'gid://shopify/MediaImage/23117955891251', + 'shopifyGid' => 'gid://shopify/MediaImage/23117955891251', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23117955891251', @@ -2171,7 +2171,7 @@ ], 'ProductVariant_productvariant_41966400143411' => [ 'id' => 64679, - 'shopifyId' => 'gid://shopify/ProductVariant/41966400143411', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966400143411', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966400143411', @@ -2226,7 +2226,7 @@ ], 'MediaImage_mediaimage_23118015758387' => [ 'id' => 65045, - 'shopifyId' => 'gid://shopify/MediaImage/23118015758387', + 'shopifyGid' => 'gid://shopify/MediaImage/23118015758387', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23118015758387', @@ -2249,7 +2249,7 @@ ], 'ProductVariant_productvariant_41966466236467' => [ 'id' => 65046, - 'shopifyId' => 'gid://shopify/ProductVariant/41966466236467', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966466236467', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966466236467', @@ -2300,7 +2300,7 @@ ], 'MediaImage_mediaimage_23118019788851' => [ 'id' => 65081, - 'shopifyId' => 'gid://shopify/MediaImage/23118019788851', + 'shopifyGid' => 'gid://shopify/MediaImage/23118019788851', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23118019788851', @@ -2323,7 +2323,7 @@ ], 'ProductVariant_productvariant_41966470201395' => [ 'id' => 65082, - 'shopifyId' => 'gid://shopify/ProductVariant/41966470201395', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966470201395', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966470201395', @@ -2374,7 +2374,7 @@ ], 'MediaImage_mediaimage_23118023426099' => [ 'id' => 65108, - 'shopifyId' => 'gid://shopify/MediaImage/23118023426099', + 'shopifyGid' => 'gid://shopify/MediaImage/23118023426099', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23118023426099', @@ -2397,7 +2397,7 @@ ], 'ProductVariant_productvariant_41966471970867' => [ 'id' => 65109, - 'shopifyId' => 'gid://shopify/ProductVariant/41966471970867', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966471970867', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966471970867', @@ -2448,7 +2448,7 @@ ], 'MediaImage_mediaimage_23118091616307' => [ 'id' => 65621, - 'shopifyId' => 'gid://shopify/MediaImage/23118091616307', + 'shopifyGid' => 'gid://shopify/MediaImage/23118091616307', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23118091616307', @@ -2471,7 +2471,7 @@ ], 'ProductVariant_productvariant_41966558183475' => [ 'id' => 65622, - 'shopifyId' => 'gid://shopify/ProductVariant/41966558183475', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966558183475', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966558183475', @@ -2522,7 +2522,7 @@ ], 'MediaImage_mediaimage_23118095417395' => [ 'id' => 65645, - 'shopifyId' => 'gid://shopify/MediaImage/23118095417395', + 'shopifyGid' => 'gid://shopify/MediaImage/23118095417395', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23118095417395', @@ -2545,7 +2545,7 @@ ], 'ProductVariant_productvariant_41966567358515' => [ 'id' => 65646, - 'shopifyId' => 'gid://shopify/ProductVariant/41966567358515', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966567358515', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966567358515', @@ -2596,7 +2596,7 @@ ], 'MediaImage_mediaimage_23118112358451' => [ 'id' => 65774, - 'shopifyId' => 'gid://shopify/MediaImage/23118112358451', + 'shopifyGid' => 'gid://shopify/MediaImage/23118112358451', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23118112358451', @@ -2619,7 +2619,7 @@ ], 'ProductVariant_productvariant_41966582956083' => [ 'id' => 65775, - 'shopifyId' => 'gid://shopify/ProductVariant/41966582956083', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966582956083', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966582956083', @@ -2670,7 +2670,7 @@ ], 'MediaImage_mediaimage_23118148141107' => [ 'id' => 66026, - 'shopifyId' => 'gid://shopify/MediaImage/23118148141107', + 'shopifyGid' => 'gid://shopify/MediaImage/23118148141107', 'type' => 'MediaImage', 'data' => [ 'id' => 'gid://shopify/MediaImage/23118148141107', @@ -2693,7 +2693,7 @@ ], 'ProductVariant_productvariant_41966614183987' => [ 'id' => 66027, - 'shopifyId' => 'gid://shopify/ProductVariant/41966614183987', + 'shopifyGid' => 'gid://shopify/ProductVariant/41966614183987', 'type' => 'ProductVariant', 'data' => [ 'id' => 'gid://shopify/ProductVariant/41966614183987', diff --git a/tests/unit/elements/ProductTest.php b/tests/unit/elements/ProductTest.php new file mode 100644 index 00000000..3d477b81 --- /dev/null +++ b/tests/unit/elements/ProductTest.php @@ -0,0 +1,398 @@ +shopifyGid = self::PRODUCT_GID; + $product->shopifyId = self::PRODUCT_ID; + $product->shopifyStatus = $shopifyStatus; + return $product; + } + + // ------------------------------------------------------------------------- + // shopifyId + // ------------------------------------------------------------------------- + + public function testShopifyIdIsNullByDefault(): void + { + $product = new Product(); + self::assertNull($product->shopifyId); + } + + public function testShopifyIdIsInt(): void + { + $product = $this->_makeProduct(); + self::assertIsInt($product->shopifyId); + } + + public function testShopifyIdCanBeSetOnNewElement(): void + { + $product = new Product(); + $product->shopifyId = self::PRODUCT_ID; + self::assertEquals(self::PRODUCT_ID, $product->shopifyId); + } + + // ------------------------------------------------------------------------- + // shopifyGid + // ------------------------------------------------------------------------- + + public function testShopifyGidIsNullByDefault(): void + { + $product = new Product(); + self::assertNull($product->shopifyGid); + } + + public function testShopifyGidIsString(): void + { + $product = $this->_makeProduct(); + self::assertIsString($product->shopifyGid); + } + + public function testShopifyGidHasCorrectFormat(): void + { + $product = $this->_makeProduct(); + self::assertStringStartsWith('gid://shopify/Product/', $product->shopifyGid); + } + + public function testShopifyGidCanBeSetOnNewElement(): void + { + $product = new Product(); + $product->shopifyGid = self::PRODUCT_GID; + self::assertEquals(self::PRODUCT_GID, $product->shopifyGid); + } + + public function testShopifyGidNumericSegmentIsNumeric(): void + { + $product = $this->_makeProduct(); + $lastSegment = substr($product->shopifyGid, strrpos($product->shopifyGid, '/') + 1); + self::assertMatchesRegularExpression('/^\d+$/', $lastSegment); + } + + // ------------------------------------------------------------------------- + // shopifyId and shopifyGid relationship + // ------------------------------------------------------------------------- + + public function testShopifyIdMatchesNumericSegmentOfGid(): void + { + $product = $this->_makeProduct(); + $lastSegment = (int) substr($product->shopifyGid, strrpos($product->shopifyGid, '/') + 1); + self::assertEquals($lastSegment, $product->shopifyId); + } + + public function testShopifyGidContainsShopifyId(): void + { + $product = $this->_makeProduct(); + self::assertStringContainsString((string) $product->shopifyId, $product->shopifyGid); + } + + // ------------------------------------------------------------------------- + // shopifyStatus + // ------------------------------------------------------------------------- + + public function testShopifyStatusDefaultsToActive(): void + { + $product = new Product(); + self::assertEquals(Product::SHOPIFY_STATUS_ACTIVE, $product->shopifyStatus); + } + + public function testShopifyStatusActiveConstantValue(): void + { + self::assertEquals('active', Product::SHOPIFY_STATUS_ACTIVE); + } + + public function testShopifyStatusDraftConstantValue(): void + { + self::assertEquals('draft', Product::SHOPIFY_STATUS_DRAFT); + } + + public function testShopifyStatusArchivedConstantValue(): void + { + self::assertEquals('archived', Product::SHOPIFY_STATUS_ARCHIVED); + } + + public function testShopifyStatusCanBeSetToDraft(): void + { + $product = $this->_makeProduct(Product::SHOPIFY_STATUS_DRAFT); + self::assertEquals(Product::SHOPIFY_STATUS_DRAFT, $product->shopifyStatus); + } + + public function testShopifyStatusCanBeSetToArchived(): void + { + $product = $this->_makeProduct(Product::SHOPIFY_STATUS_ARCHIVED); + self::assertEquals(Product::SHOPIFY_STATUS_ARCHIVED, $product->shopifyStatus); + } + + // ------------------------------------------------------------------------- + // getStatus() + // ------------------------------------------------------------------------- + + public function testGetStatusReturnsLiveWhenEnabledAndActive(): void + { + $product = $this->_makeProduct(); + $product->enabled = true; + self::assertEquals(Product::STATUS_LIVE, $product->getStatus()); + } + + public function testGetStatusReturnsShopifyDraftWhenEnabledAndDraft(): void + { + $product = $this->_makeProduct(Product::SHOPIFY_STATUS_DRAFT); + $product->enabled = true; + self::assertEquals(Product::STATUS_SHOPIFY_DRAFT, $product->getStatus()); + } + + public function testGetStatusReturnsShopifyArchivedWhenEnabledAndArchived(): void + { + $product = $this->_makeProduct(Product::SHOPIFY_STATUS_ARCHIVED); + $product->enabled = true; + self::assertEquals(Product::STATUS_SHOPIFY_ARCHIVED, $product->getStatus()); + } + + public function testGetStatusReturnsDisabledWhenNotEnabled(): void + { + $product = $this->_makeProduct(); + $product->enabled = false; + self::assertEquals(Product::STATUS_DISABLED, $product->getStatus()); + } + + public function testGetStatusReturnsDisabledRegardlessOfShopifyStatus(): void + { + foreach ([Product::SHOPIFY_STATUS_ACTIVE, Product::SHOPIFY_STATUS_DRAFT, Product::SHOPIFY_STATUS_ARCHIVED] as $shopifyStatus) { + $product = $this->_makeProduct($shopifyStatus); + $product->enabled = false; + self::assertEquals(Product::STATUS_DISABLED, $product->getStatus(), "Expected disabled status for shopifyStatus=$shopifyStatus"); + } + } + + // ------------------------------------------------------------------------- + // tags + // ------------------------------------------------------------------------- + + public function testGetTagsReturnsEmptyArrayByDefault(): void + { + $product = new Product(); + self::assertSame([], $product->getTags()); + } + + public function testSetTagsAcceptsArray(): void + { + $product = new Product(); + $product->setTags(['sale', 'new']); + self::assertSame(['sale', 'new'], $product->getTags()); + } + + public function testSetTagsDecodesJsonString(): void + { + $product = new Product(); + $product->setTags('["sale","new"]'); + self::assertSame(['sale', 'new'], $product->getTags()); + } + + // ------------------------------------------------------------------------- + // options + // ------------------------------------------------------------------------- + + public function testGetOptionsReturnsEmptyArrayByDefault(): void + { + $product = new Product(); + self::assertSame([], $product->getOptions()); + } + + public function testSetOptionsAcceptsArray(): void + { + $product = new Product(); + $options = [['name' => 'Size', 'values' => ['S', 'M', 'L']]]; + $product->setOptions($options); + self::assertSame($options, $product->getOptions()); + } + + public function testSetOptionsDecodesJsonString(): void + { + $product = new Product(); + $product->setOptions('[{"name":"Size","values":["S","M"]}]'); + self::assertEquals('Size', $product->getOptions()[0]['name']); + self::assertSame(['S', 'M'], $product->getOptions()[0]['values']); + } + + // ------------------------------------------------------------------------- + // data + // ------------------------------------------------------------------------- + + public function testGetDataReturnsEmptyArrayByDefault(): void + { + $product = new Product(); + self::assertSame([], $product->getData()); + } + + public function testSetDataAcceptsArray(): void + { + $product = new Product(); + $product->setData(['title' => 'Test Product']); + self::assertSame(['title' => 'Test Product'], $product->getData()); + } + + public function testSetDataDecodesJsonString(): void + { + $product = new Product(); + $product->setData('{"title":"Test Product"}'); + self::assertEquals('Test Product', $product->getData()['title']); + } + + public function testSetDataNullResultsInEmptyArray(): void + { + $product = new Product(); + $product->setData(['title' => 'Test Product']); + $product->setData(null); + self::assertSame([], $product->getData()); + } + + // ------------------------------------------------------------------------- + // descriptionHtml + // ------------------------------------------------------------------------- + + public function testGetDescriptionHtmlReturnsNullWhenNoData(): void + { + $product = new Product(); + self::assertNull($product->getDescriptionHtml()); + } + + public function testGetDescriptionHtmlReturnsValueFromData(): void + { + $product = new Product(); + $product->setData(['descriptionHtml' => '

Hello

']); + self::assertEquals('

Hello

', $product->getDescriptionHtml()); + } + + // ------------------------------------------------------------------------- + // variants + // ------------------------------------------------------------------------- + + public function testGetVariantsReturnsEmptyCollectionWithNoShopifyGid(): void + { + $product = new Product(); + self::assertCount(0, $product->getVariants()); + } + + public function testSetVariantsWrapsPlainArrayInVariantCollection(): void + { + $product = $this->_makeProduct(); + $product->setVariants([]); + self::assertInstanceOf(VariantCollection::class, $product->getVariants()); + } + + public function testSetVariantsKeepsExistingVariantCollection(): void + { + $product = $this->_makeProduct(); + $collection = VariantCollection::make(); + $product->setVariants($collection); + self::assertSame($collection, $product->getVariants()); + } + + // ------------------------------------------------------------------------- + // images + // ------------------------------------------------------------------------- + + public function testGetImagesReturnsEmptyArrayWhenNoShopifyGid(): void + { + $product = new Product(); + self::assertSame([], $product->getImages()); + } + + public function testSetImagesAcceptsArray(): void + { + $product = $this->_makeProduct(); + $images = [['url' => 'https://example.com/img.jpg']]; + $product->setImages($images); + self::assertSame($images, $product->getImages()); + } + + public function testSetImagesDecodesJsonString(): void + { + $product = $this->_makeProduct(); + $product->setImages('[{"url":"https://example.com/img.jpg"}]'); + self::assertIsArray($product->getImages()); + self::assertCount(1, $product->getImages()); + } + + // ------------------------------------------------------------------------- + // metafields + // ------------------------------------------------------------------------- + + public function testGetMetafieldsReturnsEmptyArrayWhenNoShopifyGid(): void + { + $product = new Product(); + self::assertSame([], $product->getMetafields()); + } + + public function testSetMetafieldsAcceptsListArray(): void + { + $product = $this->_makeProduct(); + $product->setMetafields([['key' => 'colour', 'value' => 'red']]); + self::assertSame(['colour' => 'red'], $product->getMetafields()); + } + + public function testSetMetafieldsDecodesJsonListString(): void + { + $product = $this->_makeProduct(); + $product->setMetafields('[{"key":"colour","value":"red"}]'); + self::assertSame(['colour' => 'red'], $product->getMetafields()); + } + + public function testSetMetafieldsNormalizesRawListFormat(): void + { + $product = $this->_makeProduct(); + $product->setMetafields([['key' => 'colour', 'value' => 'red']]); + self::assertSame(['colour' => 'red'], $product->getMetafields()); + } + + public function testSetMetafieldsThrowsForAssociativeArray(): void + { + $product = new Product(); + self::expectException(\InvalidArgumentException::class); + $product->setMetafields(['colour' => 'red']); + } + + public function testSetMetafieldsThrowsForJsonEncodedAssociativeArray(): void + { + $product = new Product(); + self::expectException(\InvalidArgumentException::class); + $product->setMetafields('{"colour":"red"}'); + } + + // ------------------------------------------------------------------------- + // getCheapestVariant / getDefaultVariant + // ------------------------------------------------------------------------- + + public function testGetDefaultVariantReturnsNullWhenNoVariants(): void + { + $product = new Product(); + self::assertNull($product->getDefaultVariant()); + } + + public function testGetCheapestVariantReturnsNullWhenNoVariants(): void + { + $product = new Product(); + self::assertNull($product->getCheapestVariant()); + } +} diff --git a/tests/unit/jobs/ProcessBulkOperationDataTest.php b/tests/unit/jobs/ProcessBulkOperationDataTest.php index 28820f10..1b9129c1 100644 --- a/tests/unit/jobs/ProcessBulkOperationDataTest.php +++ b/tests/unit/jobs/ProcessBulkOperationDataTest.php @@ -79,7 +79,7 @@ public function testProcessItemCreatesShopifyDataRecordForVariant(): void $job = $this->_makeJob(); $job->callProcessItem($json); - $record = ShopifyData::findOne(['shopifyId' => $variantGid, 'parentId' => $productGid]); + $record = ShopifyData::findOne(['shopifyGid' => $variantGid, 'parentId' => $productGid]); self::assertNotNull($record); self::assertEquals('ProductVariant', $record->type); self::assertEquals($productGid, $record->parentId); @@ -107,7 +107,7 @@ public function testProcessItemUpdatesExistingShopifyDataRecord(): void ]); $job->callProcessItem($updatedJson); - $records = ShopifyData::find()->where(['shopifyId' => $variantGid, 'parentId' => $productGid])->all(); + $records = ShopifyData::find()->where(['shopifyGid' => $variantGid, 'parentId' => $productGid])->all(); // Should still only be one record (updated in place) self::assertCount(1, $records); } @@ -188,7 +188,7 @@ public function testProcessItemsFromRealBulkOperationJsonl(): void // createOrUpdateProduct requires a full element save — mock it out Plugin::getInstance()->set('products', $this->makeEmpty(Products::class, [ 'createOrUpdateProduct' => fn() => true, - 'deleteShopifyDataByShopifyId' => fn() => null, + 'deleteShopifyDataByShopifyGid' => fn() => null, ])); $job = $this->_makeJob(); @@ -207,7 +207,7 @@ public function testProcessItemsFromRealBulkOperationJsonl(): void self::assertEquals(60, $total); // Product row - $productRow = ShopifyData::find()->where(['shopifyId' => $productGid, 'type' => 'Product'])->one(); + $productRow = ShopifyData::find()->where(['shopifyGid' => $productGid, 'type' => 'Product'])->one(); self::assertNotNull($productRow); self::assertNull($productRow->parentId); @@ -245,7 +245,7 @@ public function testBeforeClearDataAllDeletesAllShopifyData(): void { // Insert a couple of rows that should be wiped \Yii::$app->db->createCommand()->insert(\craft\shopify\db\Table::DATA, [ - 'shopifyId' => 'gid://shopify/Product/before-clear-test-001', + 'shopifyGid' => 'gid://shopify/Product/before-clear-test-001', 'type' => 'Product', 'data' => json_encode(['id' => 'gid://shopify/Product/before-clear-test-001']), 'parentId' => null, @@ -256,7 +256,7 @@ public function testBeforeClearDataAllDeletesAllShopifyData(): void $service = Plugin::getInstance()->getBulkOperations(); $model = new BulkOperation(); - $model->shopifyId = self::BULK_OP_GID; + $model->shopifyGid = self::BULK_OP_GID; $model->query = 'query {}'; $model->clearData = 'none'; $model->setStatus(BulkOperationStatus::Created); @@ -271,7 +271,7 @@ public function testBeforeClearDataAllDeletesAllShopifyData(): void public function testBeforeClearDataNoneDoesNotDeleteShopifyData(): void { \Yii::$app->db->createCommand()->insert(\craft\shopify\db\Table::DATA, [ - 'shopifyId' => 'gid://shopify/Product/before-none-test-001', + 'shopifyGid' => 'gid://shopify/Product/before-none-test-001', 'type' => 'Product', 'data' => json_encode(['id' => 'gid://shopify/Product/before-none-test-001']), 'parentId' => null, @@ -284,7 +284,7 @@ public function testBeforeClearDataNoneDoesNotDeleteShopifyData(): void $service = Plugin::getInstance()->getBulkOperations(); $model = new BulkOperation(); - $model->shopifyId = self::BULK_OP_GID; + $model->shopifyGid = self::BULK_OP_GID; $model->query = 'query {}'; $model->clearData = 'none'; $model->setStatus(BulkOperationStatus::Created); @@ -303,7 +303,7 @@ public function testBeforeClearDataNoneDoesNotDeleteShopifyData(): void private function _makeJob(string $clearData = 'none'): TestableProcessBulkOperationData { return new TestableProcessBulkOperationData([ - 'bulkOperationShopifyId' => self::BULK_OP_GID, + 'bulkOperationShopifyGid' => self::BULK_OP_GID, 'dataUrl' => 'https://storage.example.com/data.jsonl', 'objectCount' => 0, 'clearData' => $clearData, @@ -324,7 +324,7 @@ public function callProcessItem(mixed $item): void public function callBefore(): void { // Call only our override, not BaseBatchedJob::before() which requires queue context - $bulkOperation = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyId($this->bulkOperationShopifyId); + $bulkOperation = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyGid($this->bulkOperationShopifyGid); if (!$bulkOperation) { return; @@ -336,7 +336,7 @@ public function callBefore(): void if ($this->clearData === \craft\shopify\records\BulkOperation::CLEAR_DATA_ALL) { ShopifyData::deleteAll(); } elseif ($this->clearData !== \craft\shopify\records\BulkOperation::CLEAR_DATA_NONE) { - Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyId($this->clearData); + Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyGid($this->clearData); } } } diff --git a/tests/unit/models/BulkOperationTest.php b/tests/unit/models/BulkOperationTest.php new file mode 100644 index 00000000..5cb24220 --- /dev/null +++ b/tests/unit/models/BulkOperationTest.php @@ -0,0 +1,85 @@ + ['class' => BulkOperationsFixture::class], + ]; + } + + private function _getFixtureOp(): BulkOperation + { + return Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyGid(self::FIXTURE_GID); + } + + // ------------------------------------------------------------------------- + // shopifyGid + // ------------------------------------------------------------------------- + + public function testShopifyGidIsString(): void + { + $op = $this->_getFixtureOp(); + self::assertIsString($op->shopifyGid); + } + + public function testShopifyGidHasCorrectFormat(): void + { + $op = $this->_getFixtureOp(); + self::assertStringStartsWith('gid://shopify/BulkOperation/', $op->shopifyGid); + } + + public function testShopifyGidMatchesFixture(): void + { + $op = $this->_getFixtureOp(); + self::assertEquals(self::FIXTURE_GID, $op->shopifyGid); + } + + public function testShopifyGidNumericSegmentIsNumeric(): void + { + $op = $this->_getFixtureOp(); + $lastSegment = substr($op->shopifyGid, strrpos($op->shopifyGid, '/') + 1); + self::assertMatchesRegularExpression('/^\d+$/', $lastSegment); + } + + // ------------------------------------------------------------------------- + // shopifyGid set on new model + // ------------------------------------------------------------------------- + + public function testShopifyGidCanBeSetOnNewModel(): void + { + $model = new BulkOperation(); + $model->shopifyGid = self::FIXTURE_GID; + + self::assertEquals(self::FIXTURE_GID, $model->shopifyGid); + self::assertIsString($model->shopifyGid); + } + + public function testShopifyGidIsNullByDefault(): void + { + $model = new BulkOperation(); + self::assertNull($model->shopifyGid); + } +} diff --git a/tests/unit/models/VariantTest.php b/tests/unit/models/VariantTest.php new file mode 100644 index 00000000..9002447c --- /dev/null +++ b/tests/unit/models/VariantTest.php @@ -0,0 +1,123 @@ + ['class' => ShopifyDataFixture::class], + ]; + } + + private function _getFirstVariant(): \craft\shopify\models\Variant + { + $products = $this->_makeMockProduct(self::PRODUCT_GID); + Plugin::getInstance()->getProducts()->eagerLoadVariantsForProducts([$products]); + return $products->variants->first(); + } + + // ------------------------------------------------------------------------- + // shopifyGid + // ------------------------------------------------------------------------- + + public function testShopifyGidIsString(): void + { + $variant = $this->_getFirstVariant(); + self::assertIsString($variant->shopifyGid); + } + + public function testShopifyGidHasCorrectFormat(): void + { + $variant = $this->_getFirstVariant(); + self::assertStringStartsWith('gid://shopify/ProductVariant/', $variant->shopifyGid); + } + + public function testShopifyGidMatchesFixture(): void + { + $variant = $this->_getFirstVariant(); + self::assertEquals(self::VARIANT_GID, $variant->shopifyGid); + } + + // ------------------------------------------------------------------------- + // shopifyId + // ------------------------------------------------------------------------- + + public function testShopifyIdIsString(): void + { + $variant = $this->_getFirstVariant(); + self::assertIsString($variant->shopifyId); + } + + public function testShopifyIdIsNumeric(): void + { + $variant = $this->_getFirstVariant(); + self::assertMatchesRegularExpression('/^\d+$/', $variant->shopifyId); + } + + public function testShopifyIdMatchesFixture(): void + { + $variant = $this->_getFirstVariant(); + self::assertEquals(self::VARIANT_ID, $variant->shopifyId); + } + + // ------------------------------------------------------------------------- + // shopifyId and shopifyGid relationship + // ------------------------------------------------------------------------- + + public function testShopifyIdIsLastSegmentOfGid(): void + { + $variant = $this->_getFirstVariant(); + $lastSegment = substr($variant->shopifyGid, strrpos($variant->shopifyGid, '/') + 1); + self::assertEquals($lastSegment, $variant->shopifyId); + } + + public function testShopifyGidContainsShopifyId(): void + { + $variant = $this->_getFirstVariant(); + self::assertStringContainsString($variant->shopifyId, $variant->shopifyGid); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private function _makeMockProduct(string $gid): object + { + return new class($gid) { + public string $shopifyGid; + public ?VariantCollection $variants = null; + + public function __construct(string $gid) + { + $this->shopifyGid = $gid; + } + + public function setVariants(VariantCollection $variants): void + { + $this->variants = $variants; + } + }; + } +} diff --git a/tests/unit/services/BulkOperationsTest.php b/tests/unit/services/BulkOperationsTest.php index 5f3c4f86..7553249a 100644 --- a/tests/unit/services/BulkOperationsTest.php +++ b/tests/unit/services/BulkOperationsTest.php @@ -55,32 +55,48 @@ public function testGetAllBulkOperationsReturnsCollection(): void public function testGetAllBulkOperationsContainsFixtureData(): void { $ops = Plugin::getInstance()->getBulkOperations()->getAllBulkOperations(); - $shopifyIds = $ops->pluck('shopifyId')->all(); + $shopifyIds = $ops->pluck('shopifyGid')->all(); self::assertContains(self::FIXTURE_GID, $shopifyIds); } // ------------------------------------------------------------------------- - // getBulkOperationByShopifyId + // getBulkOperationByShopifyGid // ------------------------------------------------------------------------- - public function testGetBulkOperationByShopifyIdFindsKnownRecord(): void + public function testGetBulkOperationByShopifyGidFindsKnownRecord(): void { - $op = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyId(self::FIXTURE_GID); + $op = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyGid(self::FIXTURE_GID); self::assertNotNull($op); self::assertInstanceOf(BulkOperation::class, $op); - self::assertEquals(self::FIXTURE_GID, $op->shopifyId); + self::assertEquals(self::FIXTURE_GID, $op->shopifyGid); self::assertEquals(BulkOperationStatus::Completed, $op->getStatus()); } - public function testGetBulkOperationByShopifyIdReturnsNullForUnknownId(): void + public function testGetBulkOperationByShopifyGidReturnsNullForUnknownId(): void { - $op = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyId('gid://shopify/BulkOperation/does-not-exist'); + $op = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyGid('gid://shopify/BulkOperation/does-not-exist'); self::assertNull($op); } + // ------------------------------------------------------------------------- + // getBulkOperationByShopifyId (deprecated) + // ------------------------------------------------------------------------- + + /** + * @deprecated in 8.0.0. Use [[testGetBulkOperationByShopifyGidFindsKnownRecord()]] instead. + */ + public function testGetBulkOperationByShopifyIdDelegatesToGidMethod(): void + { + $op = Plugin::getInstance()->getBulkOperations()->getBulkOperationByShopifyId(self::FIXTURE_GID); + + self::assertNotNull($op); + self::assertInstanceOf(BulkOperation::class, $op); + self::assertEquals(self::FIXTURE_GID, $op->shopifyGid); + } + // ------------------------------------------------------------------------- // saveBulkOperation // ------------------------------------------------------------------------- @@ -98,12 +114,12 @@ public function testSaveBulkOperationCreatesNewRecord(): void public function testSaveBulkOperationUpdatesExistingRecord(): void { $service = Plugin::getInstance()->getBulkOperations(); - $op = $service->getBulkOperationByShopifyId(self::FIXTURE_GID); + $op = $service->getBulkOperationByShopifyGid(self::FIXTURE_GID); $op->objectCount = 9999; $service->saveBulkOperation($op, false); - $reloaded = $service->getBulkOperationByShopifyId(self::FIXTURE_GID); + $reloaded = $service->getBulkOperationByShopifyGid(self::FIXTURE_GID); self::assertEquals(9999, $reloaded->objectCount); } @@ -121,7 +137,7 @@ public function testDeleteBulkOperationByIdRemovesRecord(): void $result = $service->deleteBulkOperationById($id); self::assertTrue($result); - self::assertNull($service->getBulkOperationByShopifyId('gid://shopify/BulkOperation/delete-test-001')); + self::assertNull($service->getBulkOperationByShopifyGid('gid://shopify/BulkOperation/delete-test-001')); } public function testDeleteBulkOperationByIdReturnsTrueForMissingRecord(): void @@ -140,7 +156,7 @@ public function testCannotDeleteProcessingBulkOperation(): void $result = $service->deleteBulkOperationById($model->id); self::assertFalse($result); - self::assertNotNull($service->getBulkOperationByShopifyId('gid://shopify/BulkOperation/processing-test-001')); + self::assertNotNull($service->getBulkOperationByShopifyGid('gid://shopify/BulkOperation/processing-test-001')); } // ------------------------------------------------------------------------- @@ -180,7 +196,7 @@ public function testHandleBulkOperationFinishedMarksCanceledAsCompleted(): void $service->handleBulkOperationFinished(['admin_graphql_api_id' => $gid]); - $reloaded = $service->getBulkOperationByShopifyId($gid); + $reloaded = $service->getBulkOperationByShopifyGid($gid); self::assertNotNull($reloaded); self::assertEquals(BulkOperationStatus::Completed, $reloaded->getStatus()); self::assertEquals('CANCELED', $reloaded->shopifyStatus); @@ -207,7 +223,7 @@ public function testHandleBulkOperationFinishedStoresUrlAndObjectCountWhenComple $service->handleBulkOperationFinished(['admin_graphql_api_id' => $gid]); - $reloaded = $service->getBulkOperationByShopifyId($gid); + $reloaded = $service->getBulkOperationByShopifyGid($gid); self::assertNotNull($reloaded); self::assertEquals('COMPLETED', $reloaded->shopifyStatus); self::assertEquals($dataUrl, $reloaded->url); @@ -241,19 +257,19 @@ public function testQueueNextBulkOperationReturnsFalseWhenAlreadyProcessing(): v // Helpers // ------------------------------------------------------------------------- - private function _makeQueuedOp(string $shopifyId): BulkOperation + private function _makeQueuedOp(string $gid): BulkOperation { $model = new BulkOperation(); - $model->shopifyId = $shopifyId; + $model->shopifyGid = $gid; $model->query = 'query { products { edges { node { id } } } }'; $model->clearData = 'none'; $model->setStatus(BulkOperationStatus::Queued); return $model; } - private function _makeCreatedOp(string $shopifyId): BulkOperation + private function _makeCreatedOp(string $gid): BulkOperation { - $model = $this->_makeQueuedOp($shopifyId); + $model = $this->_makeQueuedOp($gid); $model->setStatus(BulkOperationStatus::Created); return $model; } diff --git a/tests/unit/services/ProductsTest.php b/tests/unit/services/ProductsTest.php index bceb97a8..60e898b6 100644 --- a/tests/unit/services/ProductsTest.php +++ b/tests/unit/services/ProductsTest.php @@ -10,6 +10,7 @@ use Codeception\Test\Unit; use craft\shopify\collections\VariantCollection; use craft\shopify\db\Table; +use craft\shopify\helpers\Metafield as MetafieldHelper; use craft\shopify\Plugin; use craft\shopify\records\ShopifyData; use craft\shopify\tests\fixtures\ShopifyDataFixture; @@ -63,13 +64,13 @@ public function testNormalizeShopifyGidDoesNotDoublePrefix(): void } // ------------------------------------------------------------------------- - // deleteShopifyDataByShopifyId + // deleteShopifyDataByShopifyGid // ------------------------------------------------------------------------- - public function testDeleteShopifyDataByShopifyIdRemovesProductAndChildren(): void + public function testDeleteShopifyDataByShopifyGidRemovesProductAndChildren(): void { // Verify fixture data exists before deletion - $productRow = ShopifyData::find()->where(['shopifyId' => self::PRODUCT_GID, 'type' => 'Product'])->one(); + $productRow = ShopifyData::find()->where(['shopifyGid' => self::PRODUCT_GID, 'type' => 'Product'])->one(); self::assertNotNull($productRow, 'Fixture product row must exist before deletion test.'); $variantsBefore = ShopifyData::find() @@ -77,10 +78,10 @@ public function testDeleteShopifyDataByShopifyIdRemovesProductAndChildren(): voi ->count(); self::assertGreaterThan(0, $variantsBefore, 'Fixture must have at least one variant.'); - Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyId(self::PRODUCT_GID); + Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyGid(self::PRODUCT_GID); // Product row should be gone - $productRow = ShopifyData::find()->where(['shopifyId' => self::PRODUCT_GID, 'type' => 'Product'])->one(); + $productRow = ShopifyData::find()->where(['shopifyGid' => self::PRODUCT_GID, 'type' => 'Product'])->one(); self::assertNull($productRow); // All direct children (variants, images) should also be gone @@ -88,13 +89,31 @@ public function testDeleteShopifyDataByShopifyIdRemovesProductAndChildren(): voi self::assertEquals(0, $children); } - public function testDeleteShopifyDataByShopifyIdAcceptsNumericId(): void + public function testDeleteShopifyDataByShopifyGidAcceptsNumericId(): void { // Numeric ID should be normalized to GID before lookup — no exception expected - Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyId('7136060145715'); + Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyGid('7136060145715'); $this->assertTrue(true); } + // ------------------------------------------------------------------------- + // deleteShopifyDataByShopifyId (deprecated) + // ------------------------------------------------------------------------- + + /** + * @deprecated in 8.0.0. Use [[testDeleteShopifyDataByShopifyGidRemovesProductAndChildren()]] instead. + */ + public function testDeleteShopifyDataByShopifyIdDelegatesToGidMethod(): void + { + $productRow = ShopifyData::find()->where(['shopifyGid' => self::PRODUCT_GID, 'type' => 'Product'])->one(); + self::assertNotNull($productRow, 'Fixture product row must exist before deletion test.'); + + Plugin::getInstance()->getProducts()->deleteShopifyDataByShopifyId(self::PRODUCT_GID); + + $productRow = ShopifyData::find()->where(['shopifyGid' => self::PRODUCT_GID, 'type' => 'Product'])->one(); + self::assertNull($productRow); + } + // ------------------------------------------------------------------------- // eagerLoadVariantsForProducts // ------------------------------------------------------------------------- @@ -164,7 +183,7 @@ public function testEagerLoadMetafieldsForProductsMapsKeyValuePairs(): void // Insert a product row and a metafield child \Yii::$app->db->createCommand()->insert(Table::DATA, [ - 'shopifyId' => $productGid, + 'shopifyGid' => $productGid, 'type' => 'Product', 'data' => json_encode(['id' => $productGid, 'title' => 'Test']), 'parentId' => null, @@ -174,7 +193,7 @@ public function testEagerLoadMetafieldsForProductsMapsKeyValuePairs(): void ])->execute(); \Yii::$app->db->createCommand()->insert(Table::DATA, [ - 'shopifyId' => 'gid://shopify/Metafield/test-mf-1', + 'shopifyGid' => 'gid://shopify/Metafield/test-mf-1', 'type' => 'Metafield', 'data' => json_encode(['id' => 'gid://shopify/Metafield/test-mf-1', 'key' => 'my_key', 'value' => 'my_value']), 'parentId' => $productGid, @@ -224,7 +243,7 @@ public function setImages(array $images): void public function setMetafields(array $metafields): void { - $this->metafields = $metafields; + $this->metafields = MetafieldHelper::normalizeToMap($metafields); } }; }, $shopifyGids);