diff --git a/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php b/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php index 5cdfdc88e7dc1..fdd69cc268f97 100644 --- a/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php +++ b/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php @@ -9,6 +9,8 @@ use Magento\Bundle\Helper\Catalog\Product\Configuration; use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Model\Quote\Item; use Magento\Framework\Pricing\Helper\Data; use Magento\Framework\Serialize\SerializerInterface; @@ -18,6 +20,11 @@ */ class BundleOptionDataProvider { + /** + * Option type name + */ + private const OPTION_TYPE = 'bundle'; + /** * @var Data */ @@ -33,19 +40,26 @@ class BundleOptionDataProvider */ private $configuration; + /** @var Uid */ + private $uidEncoder; + /** * @param Data $pricingHelper * @param SerializerInterface $serializer * @param Configuration $configuration + * @param Uid|null $uidEncoder */ public function __construct( Data $pricingHelper, SerializerInterface $serializer, - Configuration $configuration + Configuration $configuration, + Uid $uidEncoder = null ) { $this->pricingHelper = $pricingHelper; $this->serializer = $serializer; $this->configuration = $configuration; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -103,6 +117,7 @@ private function buildBundleOptions(array $bundleOptions, Item $item): array $options[] = [ 'id' => $bundleOption->getId(), + 'uid' => $this->uidEncoder->encode(self::OPTION_TYPE . '/' . $bundleOption->getId()), 'label' => $bundleOption->getTitle(), 'type' => $bundleOption->getType(), 'values' => $this->buildBundleOptionValues($bundleOption->getSelections(), $item), @@ -131,9 +146,15 @@ private function buildBundleOptionValues(array $selections, Item $item): array } $selectionPrice = $this->configuration->getSelectionFinalPrice($item, $selection); - + $optionDetails = [ + self::OPTION_TYPE, + $selection->getData('option_id'), + $selection->getData('selection_id'), + (int) $selection->getData('selection_qty') + ]; $values[] = [ 'id' => $selection->getSelectionId(), + 'uid' => $this->uidEncoder->encode(implode('/', $optionDetails)), 'label' => $selection->getName(), 'quantity' => $qty, 'price' => $this->pricingHelper->currency($selectionPrice, false, false), diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php index 8025cf91d28c9..0f8cdc27d2417 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php @@ -10,7 +10,9 @@ use Magento\Bundle\Model\Selection; use Magento\Bundle\Model\ResourceModel\Selection\CollectionFactory; use Magento\Bundle\Model\ResourceModel\Selection\Collection as LinkCollection; +use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Query\EnumLookup; +use Magento\Framework\GraphQl\Query\Uid; /** * Collection to fetch link data at resolution time. @@ -42,14 +44,23 @@ class Collection */ private $links = []; + /** @var Uid */ + private $uidEncoder; + /** * @param CollectionFactory $linkCollectionFactory * @param EnumLookup $enumLookup + * @param Uid|null $uidEncoder */ - public function __construct(CollectionFactory $linkCollectionFactory, EnumLookup $enumLookup) - { + public function __construct( + CollectionFactory $linkCollectionFactory, + EnumLookup $enumLookup, + Uid $uidEncoder = null + ) { $this->linkCollectionFactory = $linkCollectionFactory; $this->enumLookup = $enumLookup; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -117,6 +128,7 @@ private function fetch() : array 'price' => $link->getSelectionPriceValue(), 'position' => $link->getPosition(), 'id' => $link->getSelectionId(), + 'uid' => $this->uidEncoder->encode((string) $link->getSelectionId()), 'qty' => (float)$link->getSelectionQty(), 'quantity' => (float)$link->getSelectionQty(), 'is_default' => (bool)$link->getIsDefault(), diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php index ce5c12ce69675..c08d69a887089 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** @@ -23,6 +24,17 @@ class BundleItemOptionUid implements ResolverInterface */ private const OPTION_TYPE = 'bundle'; + /** @var Uid */ + private $uidEncoder; + + /** + * @param Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + /** * Create a option uid for entered option in "///" format * @@ -62,7 +74,6 @@ public function resolve( $content = implode('/', $optionDetails); - // phpcs:ignore Magento2.Functions.DiscouragedFunction - return base64_encode($content); + return $this->uidEncoder->encode($content); } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php index c8e2384fcb99c..fe1b47bc635b6 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php @@ -5,11 +5,12 @@ */ declare(strict_types=1); - namespace Magento\BundleGraphQl\Model\Resolver\Options; use Magento\Bundle\Model\OptionFactory; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Store\Model\StoreManagerInterface; /** @@ -17,6 +18,11 @@ */ class Collection { + /** + * Option type name + */ + private const OPTION_TYPE = 'bundle'; + /** * @var OptionFactory */ @@ -42,19 +48,26 @@ class Collection */ private $optionMap = []; + /** @var Uid */ + private $uidEncoder; + /** * @param OptionFactory $bundleOptionFactory * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param StoreManagerInterface $storeManager + * @param Uid|null $uidEncoder */ public function __construct( OptionFactory $bundleOptionFactory, JoinProcessorInterface $extensionAttributesJoinProcessor, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + Uid $uidEncoder = null ) { $this->bundleOptionFactory = $bundleOptionFactory; $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; $this->storeManager = $storeManager; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -101,7 +114,7 @@ private function fetch() : array $linkField = $optionsCollection->getConnection()->getAutoIncrementField($productTable); $optionsCollection->getSelect()->join( ['cpe' => $productTable], - 'cpe.'.$linkField.' = main_table.parent_id', + 'cpe.' . $linkField . ' = main_table.parent_id', [] )->where( "cpe.entity_id IN (?)", @@ -124,6 +137,8 @@ private function fetch() : array = $option->getTitle() === null ? $option->getDefaultTitle() : $option->getTitle(); $this->optionMap[$option->getParentId()][$option->getId()]['sku'] = $this->skuMap[$option->getParentId()]['sku']; + $this->optionMap[$option->getParentId()][$option->getId()]['uid'] + = $this->uidEncoder->encode(self::OPTION_TYPE . '/' . $option->getOptionId()); } return $this->optionMap; diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php index a21bbbb84d735..9bbe69c95f552 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\Serialize\Serializer\Json; use Magento\Sales\Api\Data\InvoiceItemInterface; @@ -23,6 +24,11 @@ */ class BundleOptions implements ResolverInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'bundle'; + /** * Serializer * @@ -35,16 +41,22 @@ class BundleOptions implements ResolverInterface */ private $valueFactory; + /** @var Uid */ + private $uidEncoder; + /** * @param ValueFactory $valueFactory * @param Json $serializer + * @param Uid $uidEncoder */ public function __construct( ValueFactory $valueFactory, - Json $serializer + Json $serializer, + Uid $uidEncoder ) { $this->valueFactory = $valueFactory; $this->serializer = $serializer; + $this->uidEncoder = $uidEncoder; } /** @@ -89,7 +101,9 @@ private function getBundleOptions( foreach ($options['bundle_options'] ?? [] as $bundleOptionId => $bundleOption) { $bundleOptions[$bundleOptionId]['label'] = $bundleOption['label'] ?? ''; $bundleOptions[$bundleOptionId]['id'] = isset($bundleOption['option_id']) ? - base64_encode($bundleOption['option_id']) : null; + $this->uidEncoder->encode((string) $bundleOption['option_id']) : null; + $bundleOptions[$bundleOptionId]['uid'] = isset($bundleOption['option_id']) ? + $this->uidEncoder->encode(self::OPTION_TYPE . '/' . $bundleOption['option_id']) : null; if (isset($bundleOption['option_id'])) { $bundleOptions[$bundleOptionId]['values'] = $this->formatBundleOptionItems( $item, @@ -127,8 +141,20 @@ private function formatBundleOptionItems( // Value Id is missing from parent, so we have to match the child to parent option if (isset($bundleChildAttributes['option_id']) && $bundleChildAttributes['option_id'] == $bundleOptionId) { + + $options = $childOrderItemOptions['info_buyRequest'] + ['bundle_option'][$bundleChildAttributes['option_id']]; + + $optionDetails = [ + self::OPTION_TYPE, + $bundleChildAttributes['option_id'], + implode(',', $options), + (int) $childOrderItemOptions['info_buyRequest']['qty'] + ]; + $optionItems[$childrenOrderItem->getItemId()] = [ - 'id' => base64_encode($childrenOrderItem->getItemId()), + 'id' => $this->uidEncoder->encode((string) $childrenOrderItem->getItemId()), + 'uid' => $this->uidEncoder->encode(implode('/', $optionDetails)), 'product_name' => $childrenOrderItem->getName(), 'product_sku' => $childrenOrderItem->getSku(), 'quantity' => $bundleChildAttributes['qty'], diff --git a/app/code/Magento/BundleGraphQl/etc/schema.graphqls b/app/code/Magento/BundleGraphQl/etc/schema.graphqls index a2cba24c7c4d4..8a60eb671b0b6 100644 --- a/app/code/Magento/BundleGraphQl/etc/schema.graphqls +++ b/app/code/Magento/BundleGraphQl/etc/schema.graphqls @@ -32,21 +32,24 @@ type BundleCartItem implements CartItemInterface { } type SelectedBundleOption { - id: Int! + id: Int! @deprecated(reason: "Use `uid` instead") + uid: ID! @doc(description: "The unique ID for a `SelectedBundleOption` object") label: String! type: String! values: [SelectedBundleOptionValue!]! } type SelectedBundleOptionValue { - id: Int! + id: Int! @doc(description: "Use `uid` instead") + uid: ID! @doc(description: "The unique ID for a `SelectedBundleOptionValue` object") label: String! quantity: Float! price: Float! } type BundleItem @doc(description: "BundleItem defines an individual item in a bundle product.") { - option_id: Int @doc(description: "An ID assigned to each type of item in a bundle product.") + option_id: Int @deprecated(reason: "Use `uid` instead") @doc(description: "An ID assigned to each type of item in a bundle product.") + uid: ID @doc(description: "The unique ID for a `BundleItem` object.") title: String @doc(description: "The display name of the item.") required: Boolean @doc(description: "Indicates whether the item must be included in the bundle.") type: String @doc(description: "The input type that the customer uses to select the item. Examples include radio button and checkbox.") @@ -56,7 +59,7 @@ type BundleItem @doc(description: "BundleItem defines an individual item in a bu } type BundleItemOption @doc(description: "BundleItemOption defines characteristics and options for a specific bundle item.") { - id: Int @doc(description: "The ID assigned to the bundled item option.") + id: Int @deprecated(reason: "Use `uid` instead") @doc(description: "The ID assigned to the bundled item option.") label: String @doc(description: "The text that identifies the bundled item option.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\Label") qty: Float @deprecated(reason: "The `qty` is deprecated. Use `quantity` instead.") @doc(description: "Indicates the quantity of this specific bundle item.") quantity: Float @doc(description: "Indicates the quantity of this specific bundle item.") @@ -66,7 +69,7 @@ type BundleItemOption @doc(description: "BundleItemOption defines characteristic price_type: PriceTypeEnum @doc(description: "One of FIXED, PERCENT, or DYNAMIC.") can_change_quantity: Boolean @doc(description: "Indicates whether the customer can change the number of items for this option.") product: ProductInterface @doc(description: "Contains details about this product option.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\BundleItemOptionUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `BundleItemOption` object.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\BundleItemOptionUid") } type BundleProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "BundleProduct defines basic features of a bundle product and contains multiple BundleItems.") { @@ -105,13 +108,15 @@ type BundleCreditMemoItem implements CreditMemoItemInterface { } type ItemSelectedBundleOption @doc(description: "A list of options of the selected bundle product") { - id: ID! @doc(description: "The unique identifier of the option") + id: ID! @deprecated(reason: "Use `uid` instead") @doc(description: "The unique ID for a `ItemSelectedBundleOption` object") + uid: ID! @doc(description: "The unique ID for a `ItemSelectedBundleOption` object") label: String! @doc(description: "The label of the option") values: [ItemSelectedBundleOptionValue] @doc(description: "A list of products that represent the values of the parent option") } type ItemSelectedBundleOptionValue @doc(description: "A list of values for the selected bundle product") { - id: ID! @doc(description: "The unique identifier of the value") + id: ID! @deprecated(reason: "Use `uid` instead") @doc(description: "The unique ID for a `ItemSelectedBundleOptionValue` object") + uid: ID! @doc(description: "The unique ID for a `ItemSelectedBundleOptionValue` object") product_name: String! @doc(description: "The name of the child bundle product") product_sku: String! @doc(description: "The SKU of the child bundle product") quantity: Float! @doc(description: "Indicates how many of this bundle product were ordered") diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php index f5d56dc9e6b0e..3f94ffd0909aa 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Controller\Product\Compare; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\NoSuchEntityException; diff --git a/app/code/Magento/Catalog/Model/CompareList.php b/app/code/Magento/Catalog/Model/CompareList.php new file mode 100644 index 0000000000000..5be30d40aacce --- /dev/null +++ b/app/code/Magento/Catalog/Model/CompareList.php @@ -0,0 +1,24 @@ +_init(ResourceModel\Product\Compare\CompareList::class); + } +} diff --git a/app/code/Magento/Catalog/Model/CompareListIdToMaskedListId.php b/app/code/Magento/Catalog/Model/CompareListIdToMaskedListId.php new file mode 100644 index 0000000000000..a911980b98894 --- /dev/null +++ b/app/code/Magento/Catalog/Model/CompareListIdToMaskedListId.php @@ -0,0 +1,58 @@ +compareListFactory = $compareListFactory; + $this->compareListResource = $compareListResource; + } + + /** + * Get listIdMask by listId + * + * @param int $listId + * + * @param int|null $customerId + * @return null|string + * @throws LocalizedException + */ + public function execute(int $listId, int $customerId = null): ?string + { + $compareList = $this->compareListFactory->create(); + $this->compareListResource->load($compareList, $listId, 'list_id'); + if ((int)$compareList->getCustomerId() !== (int)$customerId) { + throw new LocalizedException(__('This customer is not authorized to access this list')); + } + return $compareList->getListIdMask() ?? null; + } +} diff --git a/app/code/Magento/Catalog/Model/MaskedListIdToCompareListId.php b/app/code/Magento/Catalog/Model/MaskedListIdToCompareListId.php new file mode 100644 index 0000000000000..cd1506c970763 --- /dev/null +++ b/app/code/Magento/Catalog/Model/MaskedListIdToCompareListId.php @@ -0,0 +1,57 @@ +compareListFactory = $compareListFactory; + $this->compareListResource = $compareListResource; + } + + /** + * Get maskedId by listId + * + * @param string $maskedListId + * @param int $customerId + * @return int + * @throws LocalizedException + */ + public function execute(string $maskedListId, int $customerId = null): int + { + $compareList = $this->compareListFactory->create(); + $this->compareListResource->load($compareList, $maskedListId, 'list_id_mask'); + if ((int)$compareList->getCustomerId() !== (int)$customerId) { + throw new LocalizedException(__('This customer is not authorized to access this list')); + } + return (int)$compareList->getListId(); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/CompareList.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/CompareList.php new file mode 100644 index 0000000000000..4185df079d55d --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/CompareList.php @@ -0,0 +1,25 @@ +_init('catalog_compare_list', 'list_id'); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php index 7eb0552e355fc..ff29a5afa7eda 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php @@ -45,6 +45,10 @@ public function loadByProduct(\Magento\Catalog\Model\Product\Compare\Item $objec $select->where('visitor_id = ?', (int)$object->getVisitorId()); } + if ($object->getListId()) { + $select->where('list_id = ?', (int)$object->getListId()); + } + $data = $connection->fetchRow($select); if (!$data) { @@ -140,6 +144,7 @@ public function purgeVisitorByCustomer($object) /** * Update (Merge) customer data from visitor + * * After Login process * * @param \Magento\Catalog\Model\Product\Compare\Item $object diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php index 92741cf9ba88e..76f566a364769 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php @@ -31,6 +31,13 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ protected $_visitorId = 0; + /** + * List Id Filter + * + * @var int + */ + protected $listId = 0; + /** * Comparable attributes cache * @@ -156,6 +163,30 @@ public function setCustomerId($customerId) return $this; } + /** + * Set listId filter to collection + * + * @param int $listId + * + * @return $this + */ + public function setListId(int $listId) + { + $this->listId = $listId; + $this->_addJoinToSelect(); + return $this; + } + + /** + * Retrieve listId filter applied to collection + * + * @return int + */ + public function getListId(): int + { + return (int)$this->listId; + } + /** * Set visitor filter to collection * @@ -204,6 +235,10 @@ public function getConditionForJoin() return ['visitor_id' => $this->getVisitorId()]; } + if ($this->getListId()) { + return ['list_id' => $this->getListId()]; + } + return ['customer_id' => ['null' => true], 'visitor_id' => '0']; } @@ -232,6 +267,82 @@ public function _addJoinToSelect() return $this; } + /** + * Get products ids by for compare list + * + * @param int $listId + * + * @return array + */ + public function getProductsByListId(int $listId): array + { + $select = $this->getConnection()->select()-> + from( + $this->getTable('catalog_compare_item'), + 'product_id' + )->where( + 'list_id = ?', + $listId + ); + return $this->getConnection()->fetchCol($select); + } + + + /** + * Set list_id for customer compare item + * + * @param int $listId + * @param int $customerId + */ + public function setListIdToCustomerCompareItems(int $listId, int $customerId) + { + foreach ($this->getCustomerCompareItems($customerId) as $itemId) { + $this->getConnection()->update( + $this->getTable('catalog_compare_item'), + ['list_id' => $listId], + ['catalog_compare_item_id = ?' => (int)$itemId] + ); + } + } + + /** + * Remove compare list if customer compare list empty + * + * @param int|null $customerId + */ + public function removeCompareList(?int $customerId) + { + if (empty($this->getCustomerCompareItems($customerId))) { + $this->getConnection()->delete( + $this->getTable('catalog_compare_list'), + ['customer_id = ?' => $customerId] + ); + } + } + + /** + * Get customer compare items + * + * @param int|null $customerId + * @return array + */ + private function getCustomerCompareItems(?int $customerId): array + { + if ($customerId) { + $select = $this->getConnection()->select()-> + from( + $this->getTable('catalog_compare_item') + )->where( + 'customer_id = ?', + $customerId + ); + + return $this->getConnection()->fetchCol($select); + } + + return []; + } + /** * Retrieve comapre products attribute set ids * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php index 392a4aeedfeb3..76584ea2a65f3 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php @@ -149,12 +149,19 @@ public function getRelationsByChildren(array $childrenIds): array $select = $connection->select() ->from( ['cpe' => $this->getTable('catalog_product_entity')], - 'entity_id' + ['relation.child_id', 'cpe.entity_id'] )->join( ['relation' => $this->getTable('catalog_product_relation')], 'relation.parent_id = cpe.' . $linkField )->where('relation.child_id IN(?)', $childrenIds); - return $connection->fetchCol($select); + $result = $connection->fetchAll($select); + $parentIdsOfChildIds = []; + + foreach ($result as $row) { + $parentIdsOfChildIds[$row['child_id']][] = $row['entity_id']; + } + + return $parentIdsOfChildIds; } } diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml index ddd66a5bf04bd..ce34914a2f5d4 100644 --- a/app/code/Magento/Catalog/etc/db_schema.xml +++ b/app/code/Magento/Catalog/etc/db_schema.xml @@ -547,6 +547,8 @@ default="0" comment="Product ID"/> + @@ -558,6 +560,8 @@ referenceColumn="entity_id" onDelete="CASCADE"/> + @@ -573,6 +577,25 @@ + + + + + + + + + + + + + + +
uidEncoder = $uidEncoder; + } + + /** + * Composite processor that loops through available processors for arguments that come from graphql input + * + * @param string $fieldName, + * @param array $args + * @return array + * @throws GraphQlInputException + */ + public function process( + string $fieldName, + array $args + ): array { + $filterKey = 'filters'; + $idFilter = $args[$filterKey][self::ID] ?? []; + $uidFilter = $args[$filterKey][self::UID] ?? []; + if (!empty($idFilter) + && !empty($uidFilter) + && ($fieldName === 'categories' || $fieldName === 'categoryList')) { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::UID]) + ); + } elseif (!empty($uidFilter)) { + if (isset($uidFilter['eq'])) { + $args[$filterKey][self::ID]['eq'] = $this->uidEncoder->decode( + $uidFilter['eq'] + ); + } elseif (!empty($uidFilter['in'])) { + foreach ($uidFilter['in'] as $uids) { + $args[$filterKey][self::ID]['in'][] = $this->uidEncoder->decode($uids); + } + } + unset($args[$filterKey][self::UID]); + } + return $args; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php b/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php index d2c1fc8f7be9f..675118b953102 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php @@ -10,6 +10,8 @@ use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Category; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\Reflection\DataObjectProcessor; /** @@ -27,16 +29,23 @@ class Hydrator */ private $dataObjectProcessor; + /** @var Uid */ + private $uidEncoder; + /** * @param CustomAttributesFlattener $flattener * @param DataObjectProcessor $dataObjectProcessor + * @param Uid|null $uidEncoder */ public function __construct( CustomAttributesFlattener $flattener, - DataObjectProcessor $dataObjectProcessor + DataObjectProcessor $dataObjectProcessor, + Uid $uidEncoder = null ) { $this->flattener = $flattener; $this->dataObjectProcessor = $dataObjectProcessor; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -54,6 +63,7 @@ public function hydrateCategory(Category $category, $basicFieldsOnly = false) : $categoryData = $this->dataObjectProcessor->buildOutputDataArray($category, CategoryInterface::class); } $categoryData['id'] = $category->getId(); + $categoryData['uid'] = $this->uidEncoder->encode((string) $category->getId()); $categoryData['children'] = []; $categoryData['available_sort_by'] = $category->getAvailableSortBy(); $categoryData['model'] = $category; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php index 4d7ce13fd23cc..0e653995ebcab 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php @@ -7,14 +7,15 @@ namespace Magento\CatalogGraphQl\Model\Resolver; +use Magento\CatalogGraphQl\Model\Category\CategoryFilter; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree; use Magento\Framework\Exception\InputException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree; -use Magento\CatalogGraphQl\Model\Category\CategoryFilter; /** * Categories resolver, used for GraphQL category data request processing. @@ -36,19 +37,27 @@ class CategoriesQuery implements ResolverInterface */ private $extractDataFromCategoryTree; + /** + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + /** * @param CategoryTree $categoryTree * @param ExtractDataFromCategoryTree $extractDataFromCategoryTree * @param CategoryFilter $categoryFilter + * @param ArgumentsProcessorInterface $argsSelection */ public function __construct( CategoryTree $categoryTree, ExtractDataFromCategoryTree $extractDataFromCategoryTree, - CategoryFilter $categoryFilter + CategoryFilter $categoryFilter, + ArgumentsProcessorInterface $argsSelection ) { $this->categoryTree = $categoryTree; $this->extractDataFromCategoryTree = $extractDataFromCategoryTree; $this->categoryFilter = $categoryFilter; + $this->argsSelection = $argsSelection; } /** @@ -70,7 +79,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } try { - $filterResult = $this->categoryFilter->getResult($args, $store, [], $context); + $processedArgs = $this->argsSelection->process($info->fieldName, $args); + $filterResult = $this->categoryFilter->getResult($processedArgs, $store, [], $context); } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage())); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php index dcd6f816088dd..04c1754e69eb8 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php @@ -9,6 +9,8 @@ use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; /** * Breadcrumbs data provider @@ -20,13 +22,20 @@ class Breadcrumbs */ private $collectionFactory; + /** @var Uid */ + private $uidEncoder; + /** * @param CollectionFactory $collectionFactory + * @param Uid|null $uidEncoder */ public function __construct( - CollectionFactory $collectionFactory + CollectionFactory $collectionFactory, + Uid $uidEncoder = null ) { $this->collectionFactory = $collectionFactory; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -52,6 +61,7 @@ public function getData(string $categoryPath): array foreach ($collection as $category) { $breadcrumbsData[] = [ 'category_id' => $category->getId(), + 'category_uid' => $this->uidEncoder->encode((string) $category->getId()), 'category_name' => $category->getName(), 'category_level' => $category->getLevel(), 'category_url_key' => $category->getUrlKey(), diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php index 13db03bb2766b..747e05806a821 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php @@ -7,14 +7,15 @@ namespace Magento\CatalogGraphQl\Model\Resolver; +use Magento\CatalogGraphQl\Model\Category\CategoryFilter; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree; use Magento\Framework\Exception\InputException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree; -use Magento\CatalogGraphQl\Model\Category\CategoryFilter; /** * Category List resolver, used for GraphQL category data request processing. @@ -36,19 +37,27 @@ class CategoryList implements ResolverInterface */ private $extractDataFromCategoryTree; + /** + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + /** * @param CategoryTree $categoryTree * @param ExtractDataFromCategoryTree $extractDataFromCategoryTree * @param CategoryFilter $categoryFilter + * @param ArgumentsProcessorInterface $argsSelection */ public function __construct( CategoryTree $categoryTree, ExtractDataFromCategoryTree $extractDataFromCategoryTree, - CategoryFilter $categoryFilter + CategoryFilter $categoryFilter, + ArgumentsProcessorInterface $argsSelection ) { $this->categoryTree = $categoryTree; $this->extractDataFromCategoryTree = $extractDataFromCategoryTree; $this->categoryFilter = $categoryFilter; + $this->argsSelection = $argsSelection; } /** @@ -65,7 +74,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $args['filters']['ids'] = ['eq' => $store->getRootCategoryId()]; } try { - $filterResults = $this->categoryFilter->getResult($args, $store, [], $context); + $processedArgs = $this->argsSelection->process($info->fieldName, $args); + $filterResults = $this->categoryFilter->getResult($processedArgs, $store, [], $context); + $rootCategoryIds = $filterResults['category_ids']; } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage())); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php index 701ee70204486..2cfb78418bbae 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php @@ -16,9 +16,10 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * @inheritdoc - * * Fixed the id related data in the product data + * + * @deprecated Use UID + * @see \Magento\CatalogGraphQl\Model\Resolver\Product\EntityIdToUid */ class EntityIdToId implements ResolverInterface { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToUid.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToUid.php new file mode 100644 index 0000000000000..90d36e3ed8c82 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToUid.php @@ -0,0 +1,67 @@ +metadataPool = $metadataPool; + $this->uidEncoder = $uidEncoder; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var Product $product */ + $product = $value['model']; + + $productId = $product->getData( + $this->metadataPool->getMetadata(ProductInterface::class)->getIdentifierField() + ); + + return $this->uidEncoder->encode((string) $productId); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php index 7aec66ccb699f..aff8fa8a6fc6d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php @@ -8,6 +8,8 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; /** * Identity for resolved products @@ -15,7 +17,8 @@ class Identity implements IdentityInterface { /** @var string */ - private $cacheTag = \Magento\Catalog\Model\Product::CACHE_TAG; + private $cacheTagProduct = Product::CACHE_TAG; + private $cacheTagCategory = Category::CACHE_TAG; /** * Get product ids for cache tag @@ -26,12 +29,19 @@ class Identity implements IdentityInterface public function getIdentities(array $resolvedData): array { $ids = []; + $categories = $resolvedData['categories'] ?? []; $items = $resolvedData['items'] ?? []; + foreach ($categories as $category) { + $ids[] = sprintf('%s_%s', $this->cacheTagCategory, $category); + } + if (!empty($categories)) { + array_unshift($ids, $this->cacheTagCategory); + } foreach ($items as $item) { - $ids[] = sprintf('%s_%s', $this->cacheTag, $item['entity_id']); + $ids[] = sprintf('%s_%s', $this->cacheTagProduct, $item['entity_id']); } if (!empty($ids)) { - array_unshift($ids, $this->cacheTag); + array_unshift($ids, $this->cacheTagProduct); } return $ids; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php index 8843ad02320c6..3bcc69f94cda0 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php @@ -9,6 +9,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Product; use Magento\Framework\GraphQl\Config\Element\Field; @@ -22,6 +23,17 @@ */ class MediaGalleryEntries implements ResolverInterface { + /** @var Uid */ + private $uidEncoder; + + /** + * Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + /** * @inheritdoc * @@ -53,6 +65,7 @@ public function resolve( if (!empty($product->getMediaGalleryEntries())) { foreach ($product->getMediaGalleryEntries() as $key => $entry) { $mediaGalleryEntries[$key] = $entry->getData(); + $mediaGalleryEntries[$key]['uid'] = $this->uidEncoder->encode((string) $entry->getId()); if ($entry->getExtensionAttributes() && $entry->getExtensionAttributes()->getVideoContent()) { $mediaGalleryEntries[$key]['video_content'] = $entry->getExtensionAttributes()->getVideoContent()->getData(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php index 76602288039c5..f735ab846689f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php @@ -7,8 +7,10 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Option; @@ -20,6 +22,23 @@ */ class Options implements ResolverInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + + /** @var Uid */ + private $uidEncoder; + + /** + * Uid|null $uidEncoder + */ + public function __construct(Uid $uidEncoder = null) + { + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); + } + /** * @inheritdoc * @@ -55,7 +74,9 @@ public function resolve( $options[$key] = $option->getData(); $options[$key]['required'] = $option->getIsRequire(); $options[$key]['product_sku'] = $option->getProductSku(); - + $options[$key]['uid'] = $this->uidEncoder->encode( + self::OPTION_TYPE . '/' . $option->getOptionId() + ); $values = $option->getValues() ?: []; /** @var Option\Value $value */ foreach ($values as $valueKey => $value) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index ba158fab0120c..eebcbfba55b1f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -79,6 +79,11 @@ public function resolve( 'layer_type' => isset($args['search']) ? Resolver::CATALOG_LAYER_SEARCH : Resolver::CATALOG_LAYER_CATEGORY, ]; + if (isset($args['filter']['category_id'])) { + $data['categories'] = $args['filter']['category_id']['eq'] ?? $args['filter']['category_id']['in']; + $data['categories'] = is_array($data['categories']) ? $data['categories'] : [$data['categories']]; + } + return $data; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php index 4a124d69bd20f..03e8358b1ee7a 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php @@ -61,7 +61,7 @@ public function build(SearchCriteriaInterface $searchCriteria): SearchCriteriaIn foreach ($filterGroup->getFilters() as $filter) { if ($filter->getField() == CategoryProductLink::KEY_CATEGORY_ID) { $categoryFilter = $this->filterBuilder - ->setField($filter->getField()) + ->setField(CategoryProductLink::KEY_CATEGORY_ID) ->setValue($filter->getValue()) ->setConditionType($filter->getConditionType()) ->create(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUidArgsProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUidArgsProcessor.php new file mode 100644 index 0000000000000..33845c6dcce6e --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUidArgsProcessor.php @@ -0,0 +1,66 @@ +uidEncoder = $uidEncoder; + } + + /** + * Composite processor that loops through available processors for arguments that come from graphql input + * + * @param string $fieldName, + * @param array $args + * @return array + * @throws GraphQlInputException + */ + public function process( + string $fieldName, + array $args + ): array { + $idFilter = $args['filter'][self::ID] ?? []; + $uidFilter = $args['filter'][self::UID] ?? []; + if (!empty($idFilter) + && !empty($uidFilter) + && $fieldName === 'products') { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::UID]) + ); + } elseif (!empty($uidFilter)) { + if (isset($uidFilter['eq'])) { + $args['filter'][self::ID]['eq'] = $this->uidEncoder->decode((string) $uidFilter['eq']); + } elseif (!empty($uidFilter['in'])) { + foreach ($uidFilter['in'] as $uid) { + $args['filter'][self::ID]['in'][] = $this->uidEncoder->decode((string) $uid); + } + } + unset($args['filter'][self::UID]); + } + return $args; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php index d70a3aa7e63c3..0f482fd12e4e3 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\Product; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Exception\InputException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder as SearchCriteriaBuilder; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductProvider; @@ -19,6 +20,8 @@ use Magento\Search\Model\Query; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; /** * Retrieve filtered product data based off given search criteria in a format that GraphQL can interpret. @@ -50,25 +53,34 @@ class Filter implements ProductQueryInterface */ private $scopeConfig; + /** + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + /** * @param SearchResultFactory $searchResultFactory * @param ProductProvider $productDataProvider * @param FieldSelection $fieldSelection * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param ScopeConfigInterface $scopeConfig + * @param ArgumentsProcessorInterface|null $argsSelection */ public function __construct( SearchResultFactory $searchResultFactory, ProductProvider $productDataProvider, FieldSelection $fieldSelection, SearchCriteriaBuilder $searchCriteriaBuilder, - ScopeConfigInterface $scopeConfig + ScopeConfigInterface $scopeConfig, + ArgumentsProcessorInterface $argsSelection = null ) { $this->searchResultFactory = $searchResultFactory; $this->productDataProvider = $productDataProvider; $this->fieldSelection = $fieldSelection; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->scopeConfig = $scopeConfig; + $this->argsSelection = $argsSelection ? : ObjectManager::getInstance() + ->get(ArgumentsProcessorInterface::class); } /** @@ -78,6 +90,7 @@ public function __construct( * @param ResolveInfo $info * @param ContextInterface $context * @return SearchResult + * @throws GraphQlInputException */ public function getResult( array $args, @@ -86,10 +99,10 @@ public function getResult( ): SearchResult { $fields = $this->fieldSelection->getProductsFieldSelection($info); try { - $searchCriteria = $this->buildSearchCriteria($args, $info); + $searchCriteria = $this->buildSearchCriteria($info->fieldName, $args); $searchResults = $this->productDataProvider->getList($searchCriteria, $fields, false, false, $context); } catch (InputException $e) { - return $this->createEmptyResult($args); + return $this->createEmptyResult((int)$args['pageSize'], (int)$args['currentPage']); } $productArray = []; @@ -120,19 +133,22 @@ public function getResult( /** * Build search criteria from query input args * + * @param string $fieldName * @param array $args - * @param ResolveInfo $info * @return SearchCriteriaInterface + * @throws GraphQlInputException + * @throws InputException */ - private function buildSearchCriteria(array $args, ResolveInfo $info): SearchCriteriaInterface + private function buildSearchCriteria(string $fieldName, array $args): SearchCriteriaInterface { - if (!empty($args['filter'])) { - $args['filter'] = $this->formatFilters($args['filter']); + $processedArgs = $this->argsSelection->process($fieldName, $args); + if (!empty($processedArgs['filter'])) { + $processedArgs['filter'] = $this->formatFilters($processedArgs['filter']); } - $criteria = $this->searchCriteriaBuilder->build($info->fieldName, $args); - $criteria->setCurrentPage($args['currentPage']); - $criteria->setPageSize($args['pageSize']); + $criteria = $this->searchCriteriaBuilder->build($fieldName, $processedArgs); + $criteria->setCurrentPage($processedArgs['currentPage']); + $criteria->setPageSize($processedArgs['pageSize']); return $criteria; } @@ -175,17 +191,18 @@ private function formatFilters(array $filters): array * * Used for handling exceptions gracefully * - * @param array $args + * @param int $pageSize + * @param int $currentPage * @return SearchResult */ - private function createEmptyResult(array $args): SearchResult + private function createEmptyResult(int $pageSize, int $currentPage): SearchResult { return $this->searchResultFactory->create( [ 'totalCount' => 0, 'productsSearchResult' => [], - 'pageSize' => $args['pageSize'], - 'currentPage' => $args['currentPage'], + 'pageSize' => $pageSize, + 'currentPage' => $currentPage, 'totalPages' => 0, ] ); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index 4eb76fb5c2d5b..221a402cb2fff 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -12,7 +12,9 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; use Magento\Framework\Api\Search\SearchCriteriaInterface; -use Magento\Framework\Exception\InputException; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Search\Api\SearchInterface; @@ -43,6 +45,11 @@ class Search implements ProductQueryInterface */ private $fieldSelection; + /** + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + /** * @var ProductSearch */ @@ -60,6 +67,7 @@ class Search implements ProductQueryInterface * @param FieldSelection $fieldSelection * @param ProductSearch $productsProvider * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param ArgumentsProcessorInterface|null $argsSelection */ public function __construct( SearchInterface $search, @@ -67,7 +75,8 @@ public function __construct( PageSizeProvider $pageSize, FieldSelection $fieldSelection, ProductSearch $productsProvider, - SearchCriteriaBuilder $searchCriteriaBuilder + SearchCriteriaBuilder $searchCriteriaBuilder, + ArgumentsProcessorInterface $argsSelection = null ) { $this->search = $search; $this->searchResultFactory = $searchResultFactory; @@ -75,6 +84,8 @@ public function __construct( $this->fieldSelection = $fieldSelection; $this->productsProvider = $productsProvider; $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->argsSelection = $argsSelection ?: ObjectManager::getInstance() + ->get(ArgumentsProcessorInterface::class); } /** @@ -84,14 +95,13 @@ public function __construct( * @param ResolveInfo $info * @param ContextInterface $context * @return SearchResult - * @throws InputException + * @throws GraphQlInputException */ public function getResult( array $args, ResolveInfo $info, ContextInterface $context ): SearchResult { - $queryFields = $this->fieldSelection->getProductsFieldSelection($info); $searchCriteria = $this->buildSearchCriteria($args, $info); $realPageSize = $searchCriteria->getPageSize(); @@ -108,7 +118,7 @@ public function getResult( $searchResults = $this->productsProvider->getList( $searchCriteria, $itemsResults, - $queryFields, + $this->fieldSelection->getProductsFieldSelection($info), $context ); @@ -144,7 +154,8 @@ private function buildSearchCriteria(array $args, ResolveInfo $info): SearchCrit { $productFields = (array)$info->getFieldSelection(1); $includeAggregations = isset($productFields['filters']) || isset($productFields['aggregations']); - $searchCriteria = $this->searchCriteriaBuilder->build($args, $includeAggregations); + $processedArgs = $this->argsSelection->process((string) $info->fieldName, $args); + $searchCriteria = $this->searchCriteriaBuilder->build($processedArgs, $includeAggregations); return $searchCriteria; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php index 4b3e0a1a58dfd..4575c2013dc9c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php @@ -13,6 +13,9 @@ /** * Root category tree field resolver, used for GraphQL request processing. + * + * @deprecated Use the UID instead of a numeric id + * @see \Magento\CatalogGraphQl\Model\Resolver\RootCategoryUid */ class RootCategoryId implements ResolverInterface { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryUid.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryUid.php new file mode 100644 index 0000000000000..9503e9f09b03c --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryUid.php @@ -0,0 +1,38 @@ +uidEncoder = $uidEncoder; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + return $this->uidEncoder->encode((string) $context->getExtensionAttributes()->getStore()->getRootCategoryId()); + } +} diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index fd3a834bff160..8c6fac0fe621c 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -67,6 +67,15 @@ + + + + Magento\CatalogGraphQl\Model\Resolver\Products\Query\CategoryUidArgsProcessor + Magento\CatalogGraphQl\Model\Category\CategoryUidsArgsProcessor + + + + diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 812965228682f..79281ff42cf26 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -83,27 +83,28 @@ interface ProductLinksInterface @typeResolver(class: "Magento\\CatalogGraphQl\\M } interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "The ProductInterface contains attributes that are common to all types of products. Note that descriptions may not be available for custom and EAV attributes.") { - id: Int @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") + id: Int @deprecated(reason: "Use the `uid` field instead.") @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") + uid: ID! @doc(description: "The unique ID for a `ProductInterface` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToUid") name: String @doc(description: "The product name. Customers use this name to identify the product.") sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: ComplexTextValue @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") short_description: ComplexTextValue @doc(description: "A short description of the product. Its use depends on the theme.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") special_price: Float @doc(description: "The discounted price of the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\SpecialPrice") - special_from_date: String @doc(description: "The beginning date that a product has a special price.") + special_from_date: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "The beginning date that a product has a special price.") special_to_date: String @doc(description: "The end date that a product has a special price.") - attribute_set_id: Int @doc(description: "The attribute set assigned to the product.") + attribute_set_id: Int @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "The attribute set assigned to the product.") meta_title: String @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists.") meta_keyword: String @doc(description: "A comma-separated list of keywords that are visible only to search engines.") meta_description: String @doc(description: "A brief overview of the product for search results listings, maximum 255 characters.") image: ProductImage @doc(description: "The relative path to the main image on the product page.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") small_image: ProductImage @doc(description: "The relative path to the small image, which is used on catalog pages.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") thumbnail: ProductImage @doc(description: "The relative path to the product's thumbnail image.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") - new_from_date: String @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") - new_to_date: String @doc(description: "The end date for new product listings.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") + new_from_date: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") + new_to_date: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "The end date for new product listings.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") tier_price: Float @deprecated(reason: "Use price_tiers for product tier price information.") @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") options_container: String @doc(description: "If the product has multiple options, determines where they appear on the product page.") - created_at: String @doc(description: "Timestamp indicating when the product was created.") - updated_at: String @doc(description: "Timestamp indicating when the product was updated.") + created_at: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "Timestamp indicating when the product was created.") + updated_at: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "Timestamp indicating when the product was updated.") country_of_manufacture: String @doc(description: "The product's country of origin.") type_id: String @doc(description: "One of simple, virtual, bundle, downloadable, grouped, or configurable.") @deprecated(reason: "Use __typename instead.") websites: [Website] @doc(description: "An array of websites in which the product is available.") @deprecated(reason: "The field should not be used on the storefront.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Websites") @@ -132,7 +133,7 @@ type CustomizableAreaValue @doc(description: "CustomizableAreaValue defines the price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableAreaValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") } type CategoryTree implements CategoryInterface @doc(description: "Category Tree implementation.") { @@ -154,7 +155,7 @@ type CustomizableDateValue @doc(description: "CustomizableDateValue defines the price: Float @doc(description: "The price assigned to this option.") price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableDateValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") } type CustomizableDropDownOption implements CustomizableOptionInterface @doc(description: "CustomizableDropDownOption contains information about a drop down menu that is defined as part of a customizable option.") { @@ -168,7 +169,7 @@ type CustomizableDropDownValue @doc(description: "CustomizableDropDownValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the option is displayed.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableDropDownValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") } type CustomizableMultipleOption implements CustomizableOptionInterface @doc(description: "CustomizableMultipleOption contains information about a multiselect that is defined as part of a customizable option.") { @@ -182,7 +183,7 @@ type CustomizableMultipleValue @doc(description: "CustomizableMultipleValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the option is displayed.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") + uid: ID! @doc(description: "The unique ID for a `CustomizableMultipleValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") } type CustomizableFieldOption implements CustomizableOptionInterface @doc(description: "CustomizableFieldOption contains information about a text field that is defined as part of a customizable option.") { @@ -195,7 +196,7 @@ type CustomizableFieldValue @doc(description: "CustomizableFieldValue defines th price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableFieldValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") } type CustomizableFileOption implements CustomizableOptionInterface @doc(description: "CustomizableFileOption contains information about a file picker that is defined as part of a customizable option.") { @@ -210,7 +211,7 @@ type CustomizableFileValue @doc(description: "CustomizableFileValue defines the file_extension: String @doc(description: "The file extension to accept.") image_size_x: Int @doc(description: "The maximum width of an image.") image_size_y: Int @doc(description: "The maximum height of an image.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableFileValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") } interface MediaGalleryInterface @doc(description: "Contains basic information about a product image or video.") @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\MediaGalleryTypeResolver") { @@ -231,7 +232,8 @@ interface CustomizableOptionInterface @typeResolver(class: "Magento\\CatalogGrap title: String @doc(description: "The display name for this option.") required: Boolean @doc(description: "Indicates whether the option is required.") sort_order: Int @doc(description: "The order in which the option is displayed.") - option_id: Int @doc(description: "Option ID.") + option_id: Int @deprecated(reason: "Use `uid` instead") @doc(description: "Option ID.") + uid: ID! @doc(description: "The unique ID for a `CustomizableOptionInterface` object.") } interface CustomizableProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "CustomizableProductInterface contains information about customizable product options.") { @@ -239,7 +241,8 @@ interface CustomizableProductInterface @typeResolver(class: "Magento\\CatalogGra } interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\CategoryInterfaceTypeResolver") @doc(description: "CategoryInterface contains the full set of attributes that can be returned in a category search.") { - id: Int @doc(description: "An ID that uniquely identifies the category.") + id: Int @deprecated(reason: "Use the `uid` argument instead.") @doc(description: "An ID that uniquely identifies the category.") + uid: ID! @doc(description: "The unique ID for a `CategoryInterface` object.") description: String @doc(description: "An optional description of the category.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryHtmlAttribute") name: String @doc(description: "The display name of the category.") path: String @doc(description: "Category Path.") @@ -249,8 +252,8 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model canonical_url: String @doc(description: "Relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Categories' is enabled") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CanonicalUrl") position: Int @doc(description: "The position of the category relative to other categories at the same level in tree.") level: Int @doc(description: "Indicates the depth of the category within the tree.") - created_at: String @doc(description: "Timestamp indicating when the category was created.") - updated_at: String @doc(description: "Timestamp indicating when the category was updated.") + created_at: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "Timestamp indicating when the category was created.") + updated_at: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "Timestamp indicating when the category was updated.") product_count: Int @doc(description: "The number of products in the category that are marked as visible. By default, in complex products, parent products are visible, but their child products are not.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\ProductsCount") default_sort_by: String @doc(description: "The attribute to use for sorting.") products( @@ -261,8 +264,9 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs") } -type Breadcrumb @doc(description: "Breadcrumb item."){ - category_id: Int @doc(description: "Category ID.") +type Breadcrumb @doc(description: "Breadcrumb item.") { + category_id: Int @deprecated(reason: "Use the `category_uid` argument instead.") @doc(description: "Category ID.") + category_uid: ID! @doc(description: "The unique ID for a `Breadcrumb` object.") category_name: String @doc(description: "Category name.") category_level: Int @doc(description: "Category level.") category_url_key: String @doc(description: "Category URL key.") @@ -280,7 +284,7 @@ type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines th sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the radio button is displayed.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableRadioValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") } type CustomizableCheckboxOption implements CustomizableOptionInterface @doc(description: "CustomizableCheckbbixOption contains information about a set of checkbox values that are defined as part of a customizable option.") { @@ -294,7 +298,7 @@ type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the checkbox value is displayed.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableCheckboxValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") } type VirtualProduct implements ProductInterface, CustomizableProductInterface @doc(description: "A virtual product is non-tangible product that does not require shipping and is not kept in inventory.") { @@ -320,13 +324,15 @@ type CategoryProducts @doc(description: "The category products object returned i } input ProductAttributeFilterInput @doc(description: "ProductAttributeFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { - category_id: FilterEqualTypeInput @doc(description: "Filter product by category id") + category_id: FilterEqualTypeInput @deprecated(reason: "Use the `category_uid` argument instead.") @doc(description: "Deprecated: use `category_uid` to filter product by category id.") + category_uid: FilterEqualTypeInput @doc(description: "Filter product by the unique ID for a `CategoryInterface` object.") } input CategoryFilterInput @doc(description: "CategoryFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { - ids: FilterEqualTypeInput @doc(description: "Filter by category ID that uniquely identifies the category.") - parent_id: FilterEqualTypeInput @doc(description: "Filter by parent category ID") + ids: FilterEqualTypeInput @deprecated(reason: "Use the `category_uid` argument instead.") @doc(description: "Deprecated: use 'category_uid' to filter uniquely identifiers of categories.") + category_uid: FilterEqualTypeInput @doc(description: "Filter by the unique category ID for a `CategoryInterface` object.") + parent_id: FilterEqualTypeInput @doc(description: "Filter by the unique parent category ID for a `CategoryInterface` object.") url_key: FilterEqualTypeInput @doc(description: "Filter by the part of the URL that identifies the category.") name: FilterMatchTypeInput @doc(description: "Filter by the display name of the category.") url_path: FilterEqualTypeInput @doc(description: "Filter by the URL path for the category.") @@ -426,7 +432,8 @@ input ProductAttributeSortInput @doc(description: "ProductAttributeSortInput spe } type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") { - id: Int @doc(description: "The identifier assigned to the object.") + id: Int @deprecated(reason: "Use `uid` instead.") @doc(description: "The identifier assigned to the object.") + uid: ID! @doc(description: "The unique ID for a `MediaGalleryEntry` object.") media_type: String @doc(description: "image or video.") label: String @doc(description: "The alt text displayed on the UI when the user points to the image.") position: Int @doc(description: "The media item's position after it has been sorted.") @@ -454,7 +461,7 @@ type LayerFilterItem implements LayerFilterItemInterface { } -type Aggregation @doc(description: "A bucket that contains information for each filterable option (such as price, category ID, and custom attributes).") { +type Aggregation @doc(description: "A bucket that contains information for each filterable option (such as price, category `UID`, and custom attributes).") { count: Int @doc(description: "The number of options in the aggregation group.") label: String @doc(description: "The aggregation display name.") attribute_code: String! @doc(description: "Attribute code of the aggregation group.") @@ -491,7 +498,8 @@ type StoreConfig @doc(description: "The type contains information about a store grid_per_page : Int @doc(description: "Products per Page on Grid Default Value.") list_per_page : Int @doc(description: "Products per Page on List Default Value.") catalog_default_sort_by : String @doc(description: "Default Sort By.") - root_category_id: Int @doc(description: "The ID of the root category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\RootCategoryId") + root_category_id: Int @deprecated(reason: "Use `root_category_uid` instead") @doc(description: "The ID of the root category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\RootCategoryId") + root_category_uid: ID @doc(description: "The unique ID for a `CategoryInterface` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\RootCategoryUid") } type SimpleWishlistItem implements WishlistItemInterface @doc(description: "A simple product wish list Item") { diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php index 59708d90c23b7..9429a24199b47 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php @@ -55,7 +55,7 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): string { + ): ?string { /** @var StoreInterface $store */ $store = $context->getExtensionAttributes()->getStore(); $storeId = (int)$store->getId(); @@ -66,9 +66,9 @@ public function resolve( * Retrieve category url suffix by store * * @param int $storeId - * @return string + * @return string|null */ - private function getCategoryUrlSuffix(int $storeId): string + private function getCategoryUrlSuffix(int $storeId): ?string { if (!isset($this->categoryUrlSuffix[$storeId])) { $this->categoryUrlSuffix[$storeId] = $this->scopeConfig->getValue( diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php index 9a0193ba36367..97795db73fcf8 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php @@ -55,7 +55,7 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): string { + ): ?string { /** @var StoreInterface $store */ $store = $context->getExtensionAttributes()->getStore(); $storeId = (int)$store->getId(); @@ -66,9 +66,9 @@ public function resolve( * Retrieve product url suffix by store * * @param int $storeId - * @return string + * @return string|null */ - private function getProductUrlSuffix(int $storeId): string + private function getProductUrlSuffix(int $storeId): ?string { if (!isset($this->productUrlSuffix[$storeId])) { $this->productUrlSuffix[$storeId] = $this->scopeConfig->getValue( diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/AddProductsToCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/AddProductsToCompareList.php new file mode 100644 index 0000000000000..2aab3a0cb266e --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/AddProductsToCompareList.php @@ -0,0 +1,113 @@ +addProductToCompareList = $addProductToCompareList; + $this->getCompareList = $getCompareList; + $this->maskedListIdToCompareListId = $maskedListIdToCompareListId; + $this->getListIdByCustomerId = $getListIdByCustomerId; + } + + /** + * Add products to compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (empty($args['input']['uid'])) { + throw new GraphQlInputException(__('"uid" value must be specified.')); + } + + if (!isset($args['input']['products'])) { + throw new GraphQlInputException(__('"products" value must be specified.')); + } + + try { + $listId = $this->maskedListIdToCompareListId->execute($args['input']['uid'], $context->getUserId()); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + + if (!$listId) { + throw new GraphQlInputException(__('"uid" value does not exist')); + } + + try { + $this->addProductToCompareList->execute($listId, $args['input']['products'], $context); + } catch (\Exception $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + return $this->getCompareList->execute($listId, $context); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/AssignCompareListToCustomer.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/AssignCompareListToCustomer.php new file mode 100644 index 0000000000000..8c43fcf5e9299 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/AssignCompareListToCustomer.php @@ -0,0 +1,110 @@ +setCustomerToCompareList = $setCustomerToCompareList; + $this->maskedListIdToCompareListId = $maskedListIdToCompareListId; + $this->getCompareList = $getCompareList; + } + + /** + * Assign compare list to customer + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (empty($args['uid'])) { + throw new GraphQlInputException(__('"uid" value must be specified')); + } + + if (!$context->getUserId()) { + throw new GraphQlInputException(__('Customer must be logged')); + } + + try { + $listId = $this->maskedListIdToCompareListId->execute($args['uid']); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + if ($listId) { + try { + $result = $this->setCustomerToCompareList->execute($listId, $context->getUserId(), $context); + if ($result) { + return [ + 'result' => true, + 'compare_list' => $this->getCompareList->execute((int)$result->getListId(), $context) + ]; + } + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during assigning customer.') + ); + } + } + + return [ + 'result' => false + ]; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/CompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/CompareList.php new file mode 100644 index 0000000000000..861f3ce36d555 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/CompareList.php @@ -0,0 +1,85 @@ +getCompareList = $getCompareList; + $this->maskedListIdToCompareListId = $maskedListIdToCompareListId; + } + + /** + * Get compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @throws GraphQlInputException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (empty($args['uid'])) { + throw new GraphQlInputException(__('"uid" value must be specified')); + } + + try { + $listId = $this->maskedListIdToCompareListId->execute($args['uid'], $context->getUserId()); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + if (!$listId) { + return null; + } + + return $this->getCompareList->execute($listId, $context); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/CreateCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/CreateCompareList.php new file mode 100644 index 0000000000000..9b0e8fd18298f --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/CreateCompareList.php @@ -0,0 +1,124 @@ +mathRandom = $mathRandom; + $this->getListIdByCustomerId = $getListIdByCustomerId; + $this->addProductToCompareList = $addProductToCompareList; + $this->getCompareList = $getCompareList; + $this->createCompareList = $createCompareList; + } + + /** + * Create compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws GraphQlInputException + * @throws LocalizedException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $customerId = $context->getUserId(); + $products = !empty($args['input']['products']) ? $args['input']['products'] : []; + $generatedListId = $this->mathRandom->getUniqueHash(); + $listId = 0; + + try { + if ((0 === $customerId || null === $customerId)) { + $listId = $this->createCompareList->execute($generatedListId); + $this->addProductToCompareList->execute($listId, $products, $context); + } + + if ($customerId) { + $listId = $this->getListIdByCustomerId->execute($customerId); + if ($listId) { + $this->addProductToCompareList->execute($listId, $products, $context); + } else { + $listId = $this->createCompareList->execute($generatedListId, $customerId); + $this->addProductToCompareList->execute($listId, $products, $context); + } + } + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during creating compare list') + ); + } + + return $this->getCompareList->execute($listId, $context); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/CustomerCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/CustomerCompareList.php new file mode 100644 index 0000000000000..84d0aad0c9a71 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/CustomerCompareList.php @@ -0,0 +1,73 @@ +getCompareList = $getCompareList; + $this->getListIdByCustomerId = $getListIdByCustomerId; + } + + /** + * Get customer compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $listId = $this->getListIdByCustomerId->execute((int)$context->getUserId()); + + if (!$listId) { + return null; + } + + return $this->getCompareList->execute($listId, $context); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/DeleteCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/DeleteCompareList.php new file mode 100644 index 0000000000000..a811478718985 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/DeleteCompareList.php @@ -0,0 +1,137 @@ +compareListFactory = $compareListFactory; + $this->compareListResource = $compareListResource; + $this->maskedListIdToCompareListId = $maskedListIdToCompareListId; + $this->getListIdByCustomerId = $getListIdByCustomerId; + } + + /** + * Remove compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws GraphQlInputException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (empty($args['uid'])) { + throw new GraphQlInputException(__('"uid" value must be specified')); + } + + try { + $listId = $this->maskedListIdToCompareListId->execute($args['uid'], $context->getUserId()); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + $removed = ['result' => false]; + + if ($userId = $context->getUserId()) { + $customerListId = $this->getListIdByCustomerId->execute($userId); + if ($listId === $customerListId) { + try { + $removed['result'] = $this->deleteCompareList($customerListId); + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during removing compare list') + ); + } + } + } + + if ($listId) { + try { + $removed['result'] = $this->deleteCompareList($listId); + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during removing compare list') + ); + } + } + + return $removed; + } + + /** + * Delete compare list + * + * @param int|null $listId + * @return bool + */ + private function deleteCompareList(?int $listId): bool + { + $compareList = $this->compareListFactory->create(); + $compareList->setListId($listId); + $this->compareListResource->delete($compareList); + + return true; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/RemoveProductsFromCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/RemoveProductsFromCompareList.php new file mode 100644 index 0000000000000..6e4e4d8951cb9 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/RemoveProductsFromCompareList.php @@ -0,0 +1,140 @@ +getCompareList = $getCompareList; + $this->removeFromCompareList = $removeFromCompareList; + $this->maskedListIdToCompareListId = $maskedListIdToCompareListId; + $this->getListIdByCustomerId = $getListIdByCustomerId; + } + + /** + * Remove products from compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @throws GraphQlInputException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['input']['products'])) { + throw new GraphQlInputException(__('"products" value must be specified.')); + } + + if (empty($args['input']['uid'])) { + throw new GraphQlInputException(__('"uid" value must be specified.')); + } + + try { + $listId = $this->maskedListIdToCompareListId->execute($args['input']['uid'], $context->getUserId()); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + if (!$listId) { + throw new GraphQlInputException(__('"uid" value does not exist')); + } + + if ($userId = $context->getUserId()) { + $customerListId = $this->getListIdByCustomerId->execute($userId); + if ($listId === $customerListId) { + $this->removeFromCompareList($customerListId, $args); + } + } + + try { + $this->removeFromCompareList->execute($listId, $args['input']['products']); + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during removing products from compare list') + ); + } + + return $this->getCompareList->execute($listId, $context); + } + + /** + * Remove products from compare list + * + * @param int $listId + * @param array $args + * @throws GraphQlInputException + */ + private function removeFromCompareList(int $listId, array $args): void + { + try { + $this->removeFromCompareList->execute($listId, $args['input']['products']); + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during removing products from compare list') + ); + } + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/AddToCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Service/AddToCompareList.php new file mode 100644 index 0000000000000..6f976bdd07d4d --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/AddToCompareList.php @@ -0,0 +1,106 @@ +compareItemFactory = $compareItemFactory; + $this->productRepository = $productRepository; + $this->itemCollection = $collection; + } + + /** + * Add products to compare list + * + * @param int $listId + * @param array $products + * @param ContextInterface $context + * + * @return int + * @throws \Exception + */ + public function execute(int $listId, array $products, ContextInterface $context): int + { + $storeId = (int)$context->getExtensionAttributes()->getStore()->getStoreId(); + $customerId = $context->getUserId(); + if ($customerId) { + $this->itemCollection->setListIdToCustomerCompareItems($listId, $customerId); + } + + if (count($products)) { + $existedProducts = $this->itemCollection->getProductsByListId($listId); + foreach ($products as $productId) { + if (array_search($productId, $existedProducts) === false) { + if ($this->productExists($productId)) { + $item = $this->compareItemFactory->create(); + if ($customerId) { + $item->setCustomerId($customerId); + } + $item->addProductData($productId); + $item->setStoreId($storeId); + $item->setListId($listId); + $item->save(); + } + } + } + } + + return (int)$listId; + } + + /** + * Check product exists. + * + * @param int $productId + * + * @return bool + */ + private function productExists($productId) + { + try { + $product = $this->productRepository->getById((int)$productId); + return !empty($product->getId()); + } catch (NoSuchEntityException $e) { + return false; + } + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/Collection/GetComparableItemsCollection.php b/app/code/Magento/CompareListGraphQl/Model/Service/Collection/GetComparableItemsCollection.php new file mode 100644 index 0000000000000..e766ec85248a1 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/Collection/GetComparableItemsCollection.php @@ -0,0 +1,87 @@ +itemCollectionFactory = $itemCollectionFactory; + $this->catalogProductVisibility = $catalogProductVisibility; + $this->catalogConfig = $catalogConfig; + $this->compareProduct = $compareHelper; + } + + /** + * Get collection of comparable items + * + * @param int $listId + * @param ContextInterface $context + * + * @return Collection + */ + public function execute(int $listId, ContextInterface $context): Collection + { + $this->compareProduct->setAllowUsedFlat(false); + $this->items = $this->itemCollectionFactory->create(); + $this->items->setListId($listId); + $this->items->useProductItem()->setStoreId($context->getExtensionAttributes()->getStore()->getStoreId()); + $this->items->addAttributeToSelect( + $this->catalogConfig->getProductAttributes() + )->loadComparableAttributes()->addMinimalPrice()->addTaxPercents()->setVisibility( + $this->catalogProductVisibility->getVisibleInSiteIds() + ); + + return $this->items; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/CreateCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Service/CreateCompareList.php new file mode 100644 index 0000000000000..089bdb1adef17 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/CreateCompareList.php @@ -0,0 +1,57 @@ +compareListFactory = $compareListFactory; + $this->compareListResource = $compareListResource; + } + + /** + * Created new compare list + * + * @param string $maskedId + * @param int $customerId + * + * @return int + */ + public function execute(string $maskedId, ?int $customerId = null): int + { + $compareList = $this->compareListFactory->create(); + $compareList->setListIdMask($maskedId); + $compareList->setCustomerId($customerId); + $this->compareListResource->save($compareList); + + return (int)$compareList->getListId(); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/Customer/GetListIdByCustomerId.php b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/GetListIdByCustomerId.php new file mode 100644 index 0000000000000..c6437683f6ba7 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/GetListIdByCustomerId.php @@ -0,0 +1,59 @@ +compareListFactory = $compareListFactory; + $this->resourceCompareList = $resourceCompareList; + } + + /** + * Get listId by Customer ID + * + * @param int $customerId + * + * @return int|null + */ + public function execute(int $customerId): ?int + { + if ($customerId) { + /** @var CompareList $compareList */ + $compareList = $this->compareListFactory->create(); + $this->resourceCompareList->load($compareList, $customerId, 'customer_id'); + return (int)$compareList->getListId(); + } + + return null; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/Customer/SetCustomerToCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/SetCustomerToCompareList.php new file mode 100644 index 0000000000000..72216c6c70a16 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/SetCustomerToCompareList.php @@ -0,0 +1,117 @@ +validateCustomer = $validateCustomer; + $this->compareListFactory = $compareListFactory; + $this->resourceCompareList = $resourceCompareList; + $this->getListIdByCustomerId = $getListIdByCustomerId; + $this->itemCollectionFactory = $itemCollectionFactory; + $this->addProductToCompareList = $addProductToCompareList; + } + + /** + * Set customer to compare list + * + * @param int $listId + * @param int $customerId + * + * @return CompareList + * + * @throws GraphQlAuthenticationException + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute(int $listId, int $customerId, ContextInterface $context): ?CompareList + { + if ($this->validateCustomer->execute($customerId)) { + /** @var CompareList $compareListModel */ + $compareList = $this->compareListFactory->create(); + $customerListId = $this->getListIdByCustomerId->execute($customerId); + $this->resourceCompareList->load($compareList, $listId, 'list_id'); + if ($customerListId) { + $this->items = $this->itemCollectionFactory->create(); + $products = $this->items->getProductsByListId($listId); + $this->addProductToCompareList->execute($customerListId, $products, $context); + $this->resourceCompareList->delete($compareList); + $compareList = $this->compareListFactory->create(); + $this->resourceCompareList->load($compareList, $customerListId, 'list_id'); + return $compareList; + } + $compareList->setCustomerId($customerId); + $this->resourceCompareList->save($compareList); + return $compareList; + } + + return null; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/Customer/ValidateCustomer.php b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/ValidateCustomer.php new file mode 100644 index 0000000000000..ab16b17240b1c --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/ValidateCustomer.php @@ -0,0 +1,94 @@ +authentication = $authentication; + $this->accountManagement = $accountManagement; + $this->customerRepository = $customerRepository; + } + + /** + * Customer validate + * + * @param int $customerId + * + * @return int + * + * @throws GraphQlAuthenticationException + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute(int $customerId): int + { + try { + $customer = $this->customerRepository->getById($customerId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException( + __('Customer with id "%customer_id" does not exist.', ['customer_id' => $customerId]), + $e + ); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + if (true === $this->authentication->isLocked($customerId)) { + throw new GraphQlAuthenticationException(__('The account is locked.')); + } + + try { + $confirmationStatus = $this->accountManagement->getConfirmationStatus($customerId); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + if ($confirmationStatus === AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED) { + throw new GraphQlAuthenticationException(__("This account isn't confirmed. Verify and try again.")); + } + + return (int)$customer->getId(); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableAttributes.php b/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableAttributes.php new file mode 100644 index 0000000000000..5b051a3825aac --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableAttributes.php @@ -0,0 +1,53 @@ +comparableItemsCollection = $comparableItemsCollection; + } + + /** + * Get comparable attributes + * + * @param int $listId + * @param ContextInterface $context + * + * @return array + */ + public function execute(int $listId, ContextInterface $context): array + { + $attributes = []; + $itemsCollection = $this->comparableItemsCollection->execute($listId, $context); + foreach ($itemsCollection->getComparableAttributes() as $item) { + $attributes[] = [ + 'code' => $item->getAttributeCode(), + 'label' => $item->getStoreLabel() + ]; + } + + return $attributes; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableItems.php b/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableItems.php new file mode 100644 index 0000000000000..1cf42553718fd --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableItems.php @@ -0,0 +1,122 @@ +blockListCompare = $listCompare; + $this->comparableItemsCollection = $comparableItemsCollection; + $this->productRepository = $productRepository; + } + + /** + * Get comparable items + * + * @param int $listId + * @param ContextInterface $context + * + * @return array + * @throws GraphQlInputException + */ + public function execute(int $listId, ContextInterface $context) + { + $items = []; + foreach ($this->comparableItemsCollection->execute($listId, $context) as $item) { + /** @var Product $item */ + $items[] = [ + 'uid' => $item->getId(), + 'product' => $this->getProductData((int)$item->getId()), + 'attributes' => $this->getProductComparableAttributes($listId, $item, $context) + ]; + } + + return $items; + } + + /** + * Get product data + * + * @param int $productId + * + * @return array + * + * @throws GraphQlInputException + */ + private function getProductData(int $productId): array + { + $productData = []; + try { + $item = $this->productRepository->getById($productId); + $productData = $item->getData(); + $productData['model'] = $item; + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + return $productData; + } + + /** + * Get comparable attributes for product + * + * @param int $listId + * @param Product $product + * @param ContextInterface $context + * + * @return array + */ + private function getProductComparableAttributes(int $listId, Product $product, ContextInterface $context): array + { + $attributes = []; + $itemsCollection = $this->comparableItemsCollection->execute($listId, $context); + foreach ($itemsCollection->getComparableAttributes() as $item) { + $attributes[] = [ + 'code' => $item->getAttributeCode(), + 'value' => $this->blockListCompare->getProductAttributeValue($product, $item) + ]; + } + + return $attributes; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/GetCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Service/GetCompareList.php new file mode 100644 index 0000000000000..5cef555479838 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/GetCompareList.php @@ -0,0 +1,75 @@ +comparableItemsService = $comparableItemsService; + $this->comparableAttributesService = $comparableAttributesService; + $this->compareListIdToMaskedListId = $compareListIdToMaskedListId; + } + + /** + * Get compare list information + * + * @param int $listId + * @param ContextInterface $context + * + * @return array + * @throws GraphQlInputException + */ + public function execute(int $listId, ContextInterface $context) + { + try { + $maskedListId = $this->compareListIdToMaskedListId->execute($listId, $context->getUserId()); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + $comparableItems = $this->comparableItemsService->execute($listId, $context); + + return [ + 'uid' => $maskedListId, + 'items' => $comparableItems, + 'attributes' => $this->comparableAttributesService->execute($listId, $context), + 'item_count' => count($comparableItems) + ]; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/RemoveFromCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Service/RemoveFromCompareList.php new file mode 100644 index 0000000000000..dda400559a412 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/RemoveFromCompareList.php @@ -0,0 +1,59 @@ +compareItemFactory = $compareItemFactory; + $this->compareItemResource = $compareItemResource; + } + + /** + * Remove products from compare list + * + * @param int $listId + * @param array $products + */ + public function execute(int $listId, array $products) + { + foreach ($products as $productId) { + /* @var $item Item */ + $item = $this->compareItemFactory->create(); + $item->setListId($listId); + $this->compareItemResource->loadByProduct($item, $productId); + if ($item->getId()) { + $this->compareItemResource->delete($item); + } + } + } +} diff --git a/app/code/Magento/CompareListGraphQl/README.md b/app/code/Magento/CompareListGraphQl/README.md new file mode 100644 index 0000000000000..ed1c38ab33a3b --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/README.md @@ -0,0 +1,4 @@ +# CompareListGraphQl module + +The CompareListGraphQl module is designed to implement compare product functionality. + diff --git a/app/code/Magento/CompareListGraphQl/composer.json b/app/code/Magento/CompareListGraphQl/composer.json new file mode 100644 index 0000000000000..dd9c998857258 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/composer.json @@ -0,0 +1,23 @@ +{ + "name": "magento/module-compare-list-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-customer": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CompareListGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/CompareListGraphQl/etc/module.xml b/app/code/Magento/CompareListGraphQl/etc/module.xml new file mode 100644 index 0000000000000..b3c330fc38df2 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/etc/module.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/app/code/Magento/CompareListGraphQl/etc/schema.graphqls b/app/code/Magento/CompareListGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..e533d476ddd59 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/etc/schema.graphqls @@ -0,0 +1,64 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type ComparableItem { + uid: ID! @doc(description: "The unique ID of an item in a compare list") + product: ProductInterface! @doc(description: "Contains details about a product in a compare list") + attributes: [ProductAttribute]! @doc(description: "An array of product attributes that can be used to compare products") +} + +type ProductAttribute { + code: String! @doc(description: "The unique identifier for a product attribute code.") + value: String! @doc(description:"The display value of the attribute") +} + +type ComparableAttribute { + code: String! @doc(description: "An attribute code that is enabled for product comparisons") + label: String! @doc(description: "The label of the attribute code") +} + +type CompareList { + uid: ID! @doc(description: "The unique ID assigned to the compare list") + items: [ComparableItem] @doc(description: "An array of products to compare") + attributes: [ComparableAttribute] @doc(description: "An array of attributes that can be used for comparing products") + item_count: Int! @doc(description: "The number of items in the compare list") +} + +type Customer { + compare_list: CompareList @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\CustomerCompareList") @doc(description: "The contents of the customer's compare list") +} + +type Query { + compareList(uid: ID!): CompareList @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\CompareList") @doc(description: "Return products that have been added to the specified compare list") +} + +type Mutation { + createCompareList(input: CreateCompareListInput): CompareList @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\CreateCompareList") @doc(description: "Creates a new compare list. The compare list is saved for logged in customers") + addProductsToCompareList(input: AddProductsToCompareListInput): CompareList @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\AddProductsToCompareList") @doc(description: "Add products to the specified compare list") + removeProductsFromCompareList(input: RemoveProductsFromCompareListInput): CompareList @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\RemoveProductsFromCompareList") @doc(description: "Remove products from the specified compare list") + assignCompareListToCustomer(uid: ID!): AssignCompareListToCustomerOutput @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\AssignCompareListToCustomer") @doc(description: "Assign the specified compare list to the logged in customer") + deleteCompareList(uid: ID!): DeleteCompareListOutput @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\DeleteCompareList") @doc(description: "Delete the specified compare list") +} + +input CreateCompareListInput { + products: [ID!] @doc(description: "An array of product IDs to add to the compare list") +} + +input AddProductsToCompareListInput { + uid: ID!, @doc(description: "The unique identifier of the compare list to modify") + products: [ID!]! @doc(description: "An array of product IDs to add to the compare list") +} + +input RemoveProductsFromCompareListInput { + uid: ID!, @doc(description: "The unique identifier of the compare list to modify") + products: [ID!]! @doc(description: "An array of product IDs to remove from the compare list") +} + +type DeleteCompareListOutput { + result: Boolean! @doc(description: "Indicates whether the compare list was successfully deleted") +} + +type AssignCompareListToCustomerOutput { + result: Boolean! + compare_list: CompareList @doc(description: "The contents of the customer's compare list") +} diff --git a/app/code/Magento/CompareListGraphQl/registration.php b/app/code/Magento/CompareListGraphQl/registration.php new file mode 100644 index 0000000000000..bb764b439273d --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/registration.php @@ -0,0 +1,14 @@ +attributeCollectionFactory = $attributeCollectionFactory; $this->productFactory = $productFactory; $this->metadataPool = $metadataPool; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -66,7 +80,7 @@ public function __construct( * * @param int $productId */ - public function addProductId(int $productId) : void + public function addProductId(int $productId): void { if (!in_array($productId, $this->productIds)) { $this->productIds[] = $productId; @@ -79,7 +93,7 @@ public function addProductId(int $productId) : void * @param int $productId * @return array */ - public function getAttributesByProductId(int $productId) : array + public function getAttributesByProductId(int $productId): array { $attributes = $this->fetch(); @@ -95,7 +109,7 @@ public function getAttributesByProductId(int $productId) : array * * @return array */ - private function fetch() : array + private function fetch(): array { if (empty($this->productIds) || !empty($this->attributeMap)) { return $this->attributeMap; @@ -121,11 +135,24 @@ private function fetch() : array $attributeData = $attribute->getData(); $this->attributeMap[$productId][$attribute->getId()] = $attribute->getData(); $this->attributeMap[$productId][$attribute->getId()]['id'] = $attribute->getId(); - $this->attributeMap[$productId][$attribute->getId()]['attribute_id_v2'] - = $attribute->getProductAttribute()->getAttributeId(); - $this->attributeMap[$productId][$attribute->getId()]['attribute_code'] - = $attribute->getProductAttribute()->getAttributeCode(); - $this->attributeMap[$productId][$attribute->getId()]['values'] = $attributeData['options']; + $this->attributeMap[$productId][$attribute->getId()]['uid'] = $this->uidEncoder->encode( + self::OPTION_TYPE . '/' . $productId . '/' . $attribute->getAttributeId() + ); + $this->attributeMap[$productId][$attribute->getId()]['attribute_id_v2'] = + $attribute->getProductAttribute()->getAttributeId(); + $this->attributeMap[$productId][$attribute->getId()]['attribute_uid'] = + $this->uidEncoder->encode((string) $attribute->getProductAttribute()->getAttributeId()); + $this->attributeMap[$productId][$attribute->getId()]['product_uid'] = + $this->uidEncoder->encode((string) $attribute->getProductId()); + $this->attributeMap[$productId][$attribute->getId()]['attribute_code'] = + $attribute->getProductAttribute()->getAttributeCode(); + $this->attributeMap[$productId][$attribute->getId()]['values'] = array_map( + function ($value) use ($attribute) { + $value['attribute_id'] = $attribute->getAttributeId(); + return $value; + }, + $attributeData['options'] + ); $this->attributeMap[$productId][$attribute->getId()]['label'] = $attribute->getProductAttribute()->getStoreLabel(); } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php index 6624a2624f1c3..92f441f61249c 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php @@ -7,9 +7,12 @@ namespace Magento\ConfigurableProductGraphQl\Model\Resolver; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Helper\Product\Configuration; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Quote\Model\Quote\Item; @@ -19,18 +22,37 @@ */ class ConfigurableCartItemOptions implements ResolverInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'configurable'; + /** * @var Configuration */ private $configurationHelper; + /** @var Uid */ + private $uidEncoder; + + /** + * @var MetadataPool + */ + private $metadataPool; + /** * @param Configuration $configurationHelper + * @param MetadataPool $metadataPool + * @param Uid $uidEncoder */ public function __construct( - Configuration $configurationHelper + Configuration $configurationHelper, + MetadataPool $metadataPool, + Uid $uidEncoder ) { $this->configurationHelper = $configurationHelper; + $this->metadataPool = $metadataPool; + $this->uidEncoder = $uidEncoder; } /** @@ -52,7 +74,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } /** @var Item $cartItem */ $cartItem = $value['model']; - + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $productLinkId = $cartItem->getProduct()->getData($linkField); $result = []; foreach ($this->configurationHelper->getOptions($cartItem) as $option) { if (isset($option['option_type'])) { @@ -61,8 +84,14 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } $result[] = [ 'id' => $option['option_id'], + 'configurable_product_option_uid' => $this->uidEncoder->encode( + self::OPTION_TYPE . '/' . $productLinkId . '/' . $option['option_id'] + ), 'option_label' => $option['label'], 'value_id' => $option['option_value'], + 'configurable_product_option_value_uid' => $this->uidEncoder->encode( + self::OPTION_TYPE . '/' . $option['option_id'] . '/' . $option['option_value'] + ), 'value_label' => $option['value'], ]; } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php index 13f31e7e2ce10..31cbe58d670b6 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** @@ -23,6 +24,17 @@ class ConfigurableAttributeUid implements ResolverInterface */ private const OPTION_TYPE = 'configurable'; + /** @var Uid */ + private $uidEncoder; + + /** + * @param Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + /** * Create a option uid for super attribute in "//" format * @@ -61,7 +73,6 @@ public function resolve( $content = implode('/', $optionDetails); - // phpcs:ignore Magento2.Functions.DiscouragedFunction - return base64_encode($content); + return $this->uidEncoder->encode($content); } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml index dc672b02e2f96..227817b3887ba 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml @@ -51,4 +51,12 @@ + + + + + checkout/cart/configurable_product_image + + + diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 6fd3132aa6645..fc177557906ee 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -19,23 +19,26 @@ type ConfigurableAttributeOption @doc(description: "ConfigurableAttributeOption label: String @doc(description: "A string that describes the configurable attribute option") code: String @doc(description: "The ID assigned to the attribute") value_index: Int @doc(description: "A unique index number assigned to the configurable product option") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes\\ConfigurableAttributeUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `ConfigurableAttributeOption` object") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes\\ConfigurableAttributeUid") } type ConfigurableProductOptions @doc(description: "ConfigurableProductOptions defines configurable attributes for the specified product") { - id: Int @doc(description: "The configurable option ID number assigned by the system") - attribute_id: String @deprecated(reason: "Use attribute_id_v2 instead") @doc(description: "The ID assigned to the attribute") - attribute_id_v2: Int @doc(description: "The ID assigned to the attribute") + id: Int @deprecated(reason: "Use uid instead") @doc(description: "The configurable option ID number assigned by the system") + uid: ID! @doc(description: "The unique ID for a `ConfigurableProductOptions` object") + attribute_id: String @deprecated(reason: "Use attribute_uid instead") @doc(description: "The ID assigned to the attribute") + attribute_id_v2: Int @deprecated(reason: "Use attribute_uid instead") @doc(description: "The ID assigned to the attribute") + attribute_uid: ID! @doc(description: "The unique ID for a `Attribute` object") attribute_code: String @doc(description: "A string that identifies the attribute") label: String @doc(description: "A string that describes the configurable product option, which is displayed on the UI") position: Int @doc(description: "A number that indicates the order in which the attribute is displayed") use_default: Boolean @doc(description: "Indicates whether the option is the default") values: [ConfigurableProductOptionsValues] @doc(description: "An array that defines the value_index codes assigned to the configurable product") - product_id: Int @doc(description: "This is the same as a product's id field") + product_id: Int @deprecated(reason: "`product_id` is not needed and can be obtained from it's parent") @doc(description: "This is the same as a product's id field") } type ConfigurableProductOptionsValues @doc(description: "ConfigurableProductOptionsValues contains the index number assigned to a configurable product option") { - value_index: Int @doc(description: "A unique index number assigned to the configurable product option") + value_index: Int @deprecated(reason: "Use `uid` instead") @doc(description: "A unique index number assigned to the configurable product option") + uid: ID @doc(description: "The unique ID for a `ConfigurableProductOptionsValues` object") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes\\ConfigurableAttributeUid") label: String @doc(description: "The label of the product") default_label: String @doc(description: "The label of the product on the default store") store_label: String @doc(description: "The label of the product on the current store") @@ -53,7 +56,7 @@ type AddConfigurableProductsToCartOutput { input ConfigurableProductCartItemInput { data: CartItemInput! - variant_sku: String @deprecated(reason: "Use CartItemInput.sku instead.") + variant_sku: String @doc(description: "Deprecated. Use CartItemInput.sku instead.") parent_sku: String @doc(description: "Configurable product SKU.") customizable_options:[CustomizableOptionInput!] } @@ -64,9 +67,11 @@ type ConfigurableCartItem implements CartItemInterface { } type SelectedConfigurableOption { - id: Int! + id: Int! @deprecated(reason: "Use SelectedConfigurableOption.configurable_product_option_uid instead") + configurable_product_option_uid: ID! @doc(description: "The unique ID for a `ConfigurableProductOptions` object") option_label: String! - value_id: Int! + value_id: Int! @deprecated(reason: "Use SelectedConfigurableOption.configurable_product_option_value_uid instead") + configurable_product_option_value_uid: ID! @doc(description: "The unique ID for a `ConfigurableProductOptionsValues` object") value_label: String! } @@ -85,3 +90,7 @@ type ConfigurableOptionAvailableForSelection @doc(description: "Configurable opt option_value_uids: [ID!]! @doc(description: "Configurable option values available for further selection.") attribute_code: String! @doc(description: "Attribute code that uniquely identifies configurable option.") } + +type StoreConfig @doc(description: "The type contains information about a store config") { + configurable_thumbnail_source : String @doc(description: "The configuration setting determines which thumbnail should be used in the cart for configurable products.") +} diff --git a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls index 6daf13f567d4b..06d7d4817ee02 100644 --- a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls @@ -24,7 +24,7 @@ type ExchangeRate { } type Country { - id: String + id: String @doc(description: "The unique ID for a `Country` object.") two_letter_abbreviation: String three_letter_abbreviation: String full_name_locale: String @@ -33,7 +33,7 @@ type Country { } type Region { - id: Int + id: Int @doc(description: "The unique ID for a `Region` object.") code: String name: String } diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 8248343fcb120..d8e9c9615b618 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -53,7 +53,7 @@ type DownloadableProductLinks @doc(description: "DownloadableProductLinks define link_type: DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get the downloadable sample") sample_type: DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get the downloadable sample") sample_file: String @deprecated(reason: "`sample_url` serves to get the downloadable sample") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") + uid: ID! @doc(description: "The unique ID for a `DownloadableProductLinks` object.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") } type DownloadableProductSamples @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") { @@ -80,7 +80,7 @@ type DownloadableCreditMemoItem implements CreditMemoItemInterface { type DownloadableItemsLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { title: String @doc(description: "The display name of the link") sort_order: Int @doc(description: "A number indicating the sort order") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") + uid: ID! @doc(description: "The unique ID for a `DownloadableItemsLinks` object.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") } type DownloadableWishlistItem implements WishlistItemInterface @doc(description: "A downloadable product wish list item") { diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml index 7195c05c0877b..865e8f223db54 100644 --- a/app/code/Magento/GraphQl/etc/di.xml +++ b/app/code/Magento/GraphQl/etc/di.xml @@ -14,6 +14,7 @@ + diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index 7366567c2b95d..65280ad1ad2aa 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -279,6 +279,6 @@ enum CurrencyEnum @doc(description: "The list of available currency codes") { } input EnteredOptionInput @doc(description: "Defines a customer-entered option") { - uid: ID! @doc(description: "An encoded ID") + uid: ID! @doc(description: "The unique ID for a `CustomizableFieldOption`, `CustomizableFileOption`, `CustomizableAreaOption`, etc. of `CustomizableOptionInterface` objects") value: String! @doc(description: "Text the customer entered") } diff --git a/app/code/Magento/LoginAsCustomerGraphQl/Model/LoginAsCustomer/CreateCustomerToken.php b/app/code/Magento/LoginAsCustomerGraphQl/Model/LoginAsCustomer/CreateCustomerToken.php new file mode 100755 index 0000000000000..a10bc10ffb825 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/Model/LoginAsCustomer/CreateCustomerToken.php @@ -0,0 +1,78 @@ +tokenModelFactory = $tokenModelFactory; + $this->customerFactory= $customerFactory; + } + + /** + * Get admin user token + * + * @param string $email + * @param StoreInterface $store + * @return array + * @throws GraphQlInputException + * @throws LocalizedException + */ + public function execute(string $email, StoreInterface $store): array + { + $customer = $this->customerFactory->create()->setWebsiteId((int)$store->getId())->loadByEmail($email); + + /* Check if customer email exist */ + if (!$customer->getId()) { + throw new GraphQlInputException( + __('Customer email provided does not exist') + ); + } + + try { + return [ + "customer_token" => $this->tokenModelFactory->create() + ->createCustomerToken($customer->getId())->getToken() + ]; + } catch (Exception $e) { + throw new LocalizedException( + __( + 'Unable to generate tokens. ' + . 'Please wait and try again later.' + ) + ); + } + } +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/RequestCustomerToken.php b/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/RequestCustomerToken.php new file mode 100755 index 0000000000000..6889f79fd1270 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/RequestCustomerToken.php @@ -0,0 +1,125 @@ +authorization = $authorization; + $this->config = $config; + $this->createCustomerToken = $createCustomerToken; + } + + /** + * Get Customer Token using email + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return Value|mixed|void + * @throws GraphQlAuthorizationException|GraphQlNoSuchEntityException|LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $isAllowedLogin = $this->authorization->isAllowed('Magento_LoginAsCustomer::login'); + $isAlllowedShoppingAssistance = $this->authorization->isAllowed('Magento_LoginAsCustomer::allow_shopping_assistance'); + $isEnabled = $this->config->isEnabled(); + + /* Get input params */ + try { + $args = $args['input']; + } catch (NoSuchEntityException $e) { + throw new GraphQlInputException(__('Check input params.')); + } + + if (empty(trim($args['customer_email'], " "))) { + throw new GraphQlInputException(__('Specify the "customer email" value.')); + } + + $this->validateUser($context); + + if (!$isAllowedLogin || !$isEnabled) { + throw new GraphQlAuthorizationException( + __('Login as Customer is disabled.') + ); + } + + if (!$isAlllowedShoppingAssistance) { + throw new GraphQlAuthorizationException( + __('Allow remote shopping assistance is disabled.') + ); + } + + return $this->createCustomerToken->execute( + $args['customer_email'], + $context->getExtensionAttributes()->getStore() + ); + } + + /** + * Check if its an admin user + * + * @param ContextInterface $context + * @throws GraphQlAuthorizationException + */ + private function validateUser(ContextInterface $context): void + { + if ($context->getUserType() !== 2 || $context->getUserId() === 0) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + } +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/isRemoteShoppingAssistanceAllowed.php b/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/isRemoteShoppingAssistanceAllowed.php new file mode 100755 index 0000000000000..6ab2a7386986d --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/isRemoteShoppingAssistanceAllowed.php @@ -0,0 +1,56 @@ +isAssistanceEnabled = $isAssistanceEnabled; + } + + /** + * Determines if remote shopping assistance is allowed for the specified customer + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return Value|mixed|void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + return $this->isAssistanceEnabled->execute((int)$value['model']->getId()); + } +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/Plugin/DataObjectHelperPlugin.php b/app/code/Magento/LoginAsCustomerGraphQl/Plugin/DataObjectHelperPlugin.php new file mode 100644 index 0000000000000..b1c54c6d00df7 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/Plugin/DataObjectHelperPlugin.php @@ -0,0 +1,65 @@ +customerExtensionFactory = $customerExtensionFactory; + } + + /** + * Add assistance_allowed extension attribute value to Customer instance. + * + * @param DataObjectHelper $subject + * @param DataObjectHelper $result + * @param mixed $dataObject + * @param array $data + * @param string $interfaceName + * @return DataObjectHelper + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterPopulateWithArray( + DataObjectHelper $subject, + DataObjectHelper $result, + Object $dataObject, + array $data, + string $interfaceName + ) { + if ($interfaceName === CustomerInterface::class + && array_key_exists('allow_remote_shopping_assistance', $data)) { + $isLoginAsCustomerEnabled = $data['allow_remote_shopping_assistance']; + $extensionAttributes = $dataObject->getExtensionAttributes(); + if (null === $extensionAttributes) { + $extensionAttributes = $this->customerExtensionFactory->create(); + } + $extensionAttributes->setAssistanceAllowed( + $isLoginAsCustomerEnabled ? IsAssistanceEnabled::ALLOWED : IsAssistanceEnabled::DENIED + ); + $dataObject->setExtensionAttributes($extensionAttributes); + } + return $result; + } +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/README.md b/app/code/Magento/LoginAsCustomerGraphQl/README.md new file mode 100755 index 0000000000000..4bedf92dfc238 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/README.md @@ -0,0 +1,3 @@ +# LoginAsCustomerGraphQl + +**LoginAsCustomerGraphQl** provides flexible login as a customer so a merchant or merchant admin can log into an end customer's account to assist them with their account. diff --git a/app/code/Magento/LoginAsCustomerGraphQl/composer.json b/app/code/Magento/LoginAsCustomerGraphQl/composer.json new file mode 100755 index 0000000000000..9b3e7ca2efbb7 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/composer.json @@ -0,0 +1,29 @@ +{ + "name": "magento/module-login-as-customer-graph-ql", + "description": "Flexible login as a customer so a merchant or merchant admin can log into an end customer's account to assist them with their account.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-login-as-customer-api": "*", + "magento/module-login-as-customer-assistance": "*", + "magento/module-integration": "*", + "magento/module-store": "*", + "magento/module-customer": "*" + }, + "suggest": { + "magento/module-login-as-customer": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\LoginAsCustomerGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/etc/di.xml b/app/code/Magento/LoginAsCustomerGraphQl/etc/di.xml new file mode 100644 index 0000000000000..e98bc71d872ca --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/etc/di.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/code/Magento/LoginAsCustomerGraphQl/etc/module.xml b/app/code/Magento/LoginAsCustomerGraphQl/etc/module.xml new file mode 100755 index 0000000000000..1d0d92eb3cbbd --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/etc/module.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/app/code/Magento/LoginAsCustomerGraphQl/etc/schema.graphqls b/app/code/Magento/LoginAsCustomerGraphQl/etc/schema.graphqls new file mode 100755 index 0000000000000..296f5b23a8b5f --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/etc/schema.graphqls @@ -0,0 +1,32 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Mutation { + generateCustomerTokenAsAdmin( + input: GenerateCustomerTokenAsAdminInput! + ): GenerateCustomerTokenAsAdminOutput + @resolver(class: "Magento\\LoginAsCustomerGraphQl\\Model\\Resolver\\RequestCustomerToken") + @doc(description: "Request a customer token so that an administrator can perform remote shopping assistance") +} + +input GenerateCustomerTokenAsAdminInput { + customer_email: String! @doc(description: "The email address of the customer requesting remote shopping assistance") +} + +type GenerateCustomerTokenAsAdminOutput { + customer_token: String! @doc(description: "The generated customer token") +} + +type Customer { + allow_remote_shopping_assistance: Boolean! + @resolver(class: "Magento\\LoginAsCustomerGraphQl\\Model\\Resolver\\isRemoteShoppingAssistanceAllowed") + @doc(description: "Indicates whether the customer has enabled remote shopping assistance") +} + +input CustomerCreateInput { + allow_remote_shopping_assistance: Boolean @doc(description: "Indicates whether the customer has enabled remote shopping assistance") +} + +input CustomerUpdateInput { + allow_remote_shopping_assistance: Boolean @doc(description: "Indicates whether the customer has enabled remote shopping assistance") +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/registration.php b/app/code/Magento/LoginAsCustomerGraphQl/registration.php new file mode 100755 index 0000000000000..0981811982e6b --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/registration.php @@ -0,0 +1,14 @@ +maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; $this->cartRepository = $cartRepository; + $this->storeRepository = $storeRepository ?: ObjectManager::getInstance()->get(StoreRepositoryInterface::class); } /** @@ -49,6 +60,7 @@ public function __construct( * @param int $storeId * @return Quote * @throws GraphQlAuthorizationException + * @throws GraphQlInputException * @throws GraphQlNoSuchEntityException * @throws NoSuchEntityException */ @@ -75,14 +87,7 @@ public function execute(string $cartHash, ?int $customerId, int $storeId): Quote throw new GraphQlNoSuchEntityException(__('The cart isn\'t active.')); } - if ((int)$cart->getStoreId() !== $storeId) { - throw new GraphQlNoSuchEntityException( - __( - 'Wrong store code specified for cart "%masked_cart_id"', - ['masked_cart_id' => $cartHash] - ) - ); - } + $this->updateCartCurrency($cart, $storeId); $cartCustomerId = (int)$cart->getCustomerId(); @@ -101,4 +106,34 @@ public function execute(string $cartHash, ?int $customerId, int $storeId): Quote } return $cart; } + + /** + * Sets cart currency based on specified store. + * + * @param Quote $cart + * @param int $storeId + * @throws GraphQlInputException + * @throws NoSuchEntityException + */ + private function updateCartCurrency(Quote $cart, int $storeId) + { + $cartStore = $this->storeRepository->getById($cart->getStoreId()); + $currentCartCurrencyCode = $cartStore->getCurrentCurrency()->getCode(); + if ((int)$cart->getStoreId() !== $storeId) { + $newStore = $this->storeRepository->getById($storeId); + if ($cartStore->getWebsite() !== $newStore->getWebsite()) { + throw new GraphQlInputException( + __('Can\'t assign cart to store in different website.') + ); + } + $cart->setStoreId($storeId); + $cart->setStoreCurrencyCode($newStore->getCurrentCurrency()); + $cart->setQuoteCurrencyCode($newStore->getCurrentCurrency()); + } elseif ($cart->getQuoteCurrencyCode() !== $currentCartCurrencyCode) { + $cart->setQuoteCurrencyCode($cartStore->getCurrentCurrency()); + } else { + return; + } + $this->cartRepository->save($cart); + } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index 71740488c4cea..fa5be95d34822 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\QuoteRepository; /** @@ -18,6 +19,16 @@ */ class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface { + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedQuoteId; + + /** + * @var GetCartForUser + */ + private $getCartForUser; + /** * @var AssignShippingAddressToCart */ @@ -34,15 +45,21 @@ class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface private $quoteRepository; /** + * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + * @param GetCartForUser $getCartForUser * @param AssignShippingAddressToCart $assignShippingAddressToCart * @param GetShippingAddress $getShippingAddress * @param QuoteRepository|null $quoteRepository */ public function __construct( + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId, + GetCartForUser $getCartForUser, AssignShippingAddressToCart $assignShippingAddressToCart, GetShippingAddress $getShippingAddress, QuoteRepository $quoteRepository = null ) { + $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; + $this->getCartForUser = $getCartForUser; $this->assignShippingAddressToCart = $assignShippingAddressToCart; $this->getShippingAddress = $getShippingAddress; $this->quoteRepository = $quoteRepository @@ -81,7 +98,10 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s throw $e; } $this->assignShippingAddressToCart->execute($cart, $shippingAddress); - // trigger quote re-evaluation after address change + + // reload updated cart & trigger quote re-evaluation after address change + $maskedId = $this->quoteIdToMaskedQuoteId->execute((int)$cart->getId()); + $cart = $this->getCartForUser->execute($maskedId, $context->getUserId(), $cart->getStoreId()); $this->quoteRepository->save($cart); } } diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemUidArgsProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemUidArgsProcessor.php new file mode 100644 index 0000000000000..1a53ae6f38190 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemUidArgsProcessor.php @@ -0,0 +1,61 @@ +uidEncoder = $uidEncoder; + } + + /** + * Process the removeItemFromCart arguments for uids + * + * @param string $fieldName, + * @param array $args + * @return array + * @throws GraphQlInputException + */ + public function process( + string $fieldName, + array $args + ): array { + $filterKey = 'input'; + $idFilter = $args[$filterKey][self::ID] ?? []; + $uidFilter = $args[$filterKey][self::UID] ?? []; + if (!empty($idFilter) + && !empty($uidFilter) + && $fieldName === 'removeItemFromCart') { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::UID]) + ); + } elseif (!empty($uidFilter)) { + $args[$filterKey][self::ID] = $this->uidEncoder->decode((string)$uidFilter); + unset($args[$filterKey][self::UID]); + } + return $args; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php new file mode 100644 index 0000000000000..85e744c026c43 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php @@ -0,0 +1,65 @@ +uidEncoder = $uidEncoder; + } + + /** + * Process the updateCartItems arguments for cart uids + * + * @param string $fieldName, + * @param array $args + * @return array + * @throws GraphQlInputException + */ + public function process( + string $fieldName, + array $args + ): array { + $filterKey = 'input'; + if (!empty($args[$filterKey]['cart_items'])) { + foreach ($args[$filterKey]['cart_items'] as $key => $cartItem) { + $idFilter = $cartItem[self::ID] ?? []; + $uidFilter = $cartItem[self::UID] ?? []; + if (!empty($idFilter) + && !empty($uidFilter) + && $fieldName === 'updateCartItems') { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::UID]) + ); + } elseif (!empty($uidFilter)) { + $args[$filterKey]['cart_items'][$key][self::ID] = $this->uidEncoder->decode((string)$uidFilter); + unset($args[$filterKey]['cart_items'][$key][self::UID]); + } + } + } + return $args; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOption.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOption.php index 3199668060ea5..9d0c19cbc8f9c 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOption.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOption.php @@ -7,7 +7,9 @@ namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Model\Quote\Item as QuoteItem; /** @@ -15,18 +17,30 @@ */ class CustomizableOption { + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + /** * @var CustomizableOptionValueInterface */ private $customizableOptionValue; + /** @var Uid */ + private $uidEncoder; + /** * @param CustomizableOptionValueInterface $customOptionValueDataProvider + * @param Uid|null $uidEncoder */ public function __construct( - CustomizableOptionValueInterface $customOptionValueDataProvider + CustomizableOptionValueInterface $customOptionValueDataProvider, + Uid $uidEncoder = null ) { $this->customizableOptionValue = $customOptionValueDataProvider; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -56,6 +70,7 @@ public function getData(QuoteItem $cartItem, int $optionId): array return [ 'id' => $option->getId(), + 'customizable_option_uid' => $this->uidEncoder->encode((string) self::OPTION_TYPE . '/' . $option->getId()), 'label' => $option->getTitle(), 'type' => $option->getType(), 'values' => $selectedOptionValueData, diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Dropdown.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Dropdown.php index 74ed403465009..d62c1951eda68 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Dropdown.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Dropdown.php @@ -9,6 +9,8 @@ use Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product\Option\Type\Select as SelectOptionType; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Quote\Model\Quote\Item\Option as SelectedOption; use Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface; @@ -18,18 +20,30 @@ */ class Dropdown implements CustomizableOptionValueInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + /** * @var PriceUnitLabel */ private $priceUnitLabel; + /** @var Uid */ + private $uidEncoder; + /** * @param PriceUnitLabel $priceUnitLabel + * @param Uid|null $uidEncoder */ public function __construct( - PriceUnitLabel $priceUnitLabel + PriceUnitLabel $priceUnitLabel, + Uid $uidEncoder = null ) { $this->priceUnitLabel = $priceUnitLabel; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -50,8 +64,15 @@ public function getData( $optionPriceType = (string)$optionValue->getPriceType(); $priceValueUnits = $this->priceUnitLabel->getData($optionPriceType); + $optionDetails = [ + self::OPTION_TYPE, + $option->getOptionId(), + $optionValue->getOptionTypeId() + ]; + $selectedOptionValueData = [ 'id' => $selectedOption->getId(), + 'customizable_option_value_uid' => $this->uidEncoder->encode((string) implode('/', $optionDetails)), 'label' => $optionTypeRenderer->getFormattedOptionValue($selectedValue), 'value' => $selectedValue, 'price' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Multiple.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Multiple.php index b3fa22c0cf61c..8831ee6304398 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Multiple.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Multiple.php @@ -9,6 +9,8 @@ use Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product\Option\Type\DefaultType; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Quote\Model\Quote\Item\Option as SelectedOption; use Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface; @@ -18,18 +20,30 @@ */ class Multiple implements CustomizableOptionValueInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + /** * @var PriceUnitLabel */ private $priceUnitLabel; + /** @var Uid */ + private $uidEncoder; + /** * @param PriceUnitLabel $priceUnitLabel + * @param Uid|null $uidEncoder */ public function __construct( - PriceUnitLabel $priceUnitLabel + PriceUnitLabel $priceUnitLabel, + Uid $uidEncoder = null ) { $this->priceUnitLabel = $priceUnitLabel; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -51,8 +65,15 @@ public function getData( $optionValue = $option->getValueById($optionId); $priceValueUnits = $this->priceUnitLabel->getData($optionValue->getPriceType()); + $optionDetails = [ + self::OPTION_TYPE, + $option->getOptionId(), + $optionValue->getOptionTypeId() + ]; + $selectedOptionValueData[] = [ 'id' => $selectedOption->getId(), + 'customizable_option_value_uid' => $this->uidEncoder->encode((string)implode('/', $optionDetails)), 'label' => $optionValue->getTitle(), 'value' => $optionId, 'price' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php index 96f11badac82e..47e616d6094b9 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php @@ -9,6 +9,8 @@ use Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product\Option\Type\Text as TextOptionType; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Quote\Model\Quote\Item\Option as SelectedOption; use Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface; @@ -18,18 +20,30 @@ */ class Text implements CustomizableOptionValueInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + /** * @var PriceUnitLabel */ private $priceUnitLabel; + /** @var Uid */ + private $uidEncoder; + /** * @param PriceUnitLabel $priceUnitLabel + * @param Uid|null $uidEncoder */ public function __construct( - PriceUnitLabel $priceUnitLabel + PriceUnitLabel $priceUnitLabel, + Uid $uidEncoder = null ) { $this->priceUnitLabel = $priceUnitLabel; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -47,6 +61,9 @@ public function getData( $selectedOptionValueData = [ 'id' => $selectedOption->getId(), + 'customizable_option_value_uid' => $this->uidEncoder->encode( + self::OPTION_TYPE . '/' . $option->getOptionId() + ), 'label' => '', 'value' => $optionTypeRenderer->getFormattedOptionValue($selectedOption->getValue()), 'price' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php index 2948994cf0ba3..2135f3798d190 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php @@ -63,6 +63,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); $this->addProductsToCart->execute($cart, $cartItems); + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ 'model' => $cart, diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php index ddd7d25943baa..6a53d976d59b3 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php @@ -85,6 +85,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value throw new LocalizedException(__($e->getMessage()), $e); } + $cart = $this->getCartForUser->execute($maskedCartId, $currentUserId, $storeId); return [ 'cart' => [ 'model' => $cart, diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php index f0d97780845e8..d4ced5b8b97b0 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php @@ -60,7 +60,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return [ 'price' => [ 'currency' => $currencyCode, - 'value' => $cartItem->getPrice(), + 'value' => $cartItem->getCalculationPrice(), ], 'row_total' => [ 'currency' => $currencyCode, diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItems.php index 39cf287a518b4..533e697c05123 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItems.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItems.php @@ -12,6 +12,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Item as QuoteItem; @@ -27,12 +28,19 @@ class CartItems implements ResolverInterface */ private $getCartProducts; + /** @var Uid */ + private $uidEncoder; + /** * @param GetCartProducts $getCartProducts + * @param Uid $uidEncoder */ - public function __construct(GetCartProducts $getCartProducts) - { + public function __construct( + GetCartProducts $getCartProducts, + Uid $uidEncoder + ) { $this->getCartProducts = $getCartProducts; + $this->uidEncoder = $uidEncoder; } /** @@ -68,6 +76,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $itemsData[] = [ 'id' => $cartItem->getItemId(), + 'uid' => $this->uidEncoder->encode((string) $cartItem->getItemId()), 'quantity' => $cartItem->getQty(), 'product' => $productData, 'model' => $cartItem, @@ -89,6 +98,7 @@ private function getCartProductsData(Quote $cart): array foreach ($products as $product) { $productsData[$product->getId()] = $product->getData(); $productsData[$product->getId()]['model'] = $product; + $productsData[$product->getId()]['uid'] = $this->uidEncoder->encode((string) $product->getId()); } return $productsData; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php index c2045d4a0e8d5..09ef1ad581876 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php @@ -15,7 +15,9 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\Quote\Model\MaskedQuoteIdToQuoteId; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; /** * @inheritdoc @@ -32,16 +34,32 @@ class RemoveItemFromCart implements ResolverInterface */ private $cartItemRepository; + /** + * @var MaskedQuoteIdToQuoteId + */ + private $maskedQuoteIdToQuoteId; + + /** + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + /** * @param GetCartForUser $getCartForUser * @param CartItemRepositoryInterface $cartItemRepository + * @param MaskedQuoteIdToQuoteId $maskedQuoteIdToQuoteId + * @param ArgumentsProcessorInterface $argsSelection */ public function __construct( GetCartForUser $getCartForUser, - CartItemRepositoryInterface $cartItemRepository + CartItemRepositoryInterface $cartItemRepository, + MaskedQuoteIdToQuoteId $maskedQuoteIdToQuoteId, + ArgumentsProcessorInterface $argsSelection ) { $this->getCartForUser = $getCartForUser; $this->cartItemRepository = $cartItemRepository; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->argsSelection = $argsSelection; } /** @@ -49,27 +67,35 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (empty($args['input']['cart_id'])) { + $processedArgs = $this->argsSelection->process($info->fieldName, $args); + if (empty($processedArgs['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); } - $maskedCartId = $args['input']['cart_id']; + $maskedCartId = $processedArgs['input']['cart_id']; + try { + $cartId = $this->maskedQuoteIdToQuoteId->execute($maskedCartId); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException( + __('Could not find a cart with ID "%masked_cart_id"', ['masked_cart_id' => $maskedCartId]) + ); + } - if (empty($args['input']['cart_item_id'])) { + if (empty($processedArgs['input']['cart_item_id'])) { throw new GraphQlInputException(__('Required parameter "cart_item_id" is missing.')); } - $itemId = $args['input']['cart_item_id']; + $itemId = $processedArgs['input']['cart_item_id']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); - $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); try { - $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); + $this->cartItemRepository->deleteById($cartId, $itemId); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__('The cart doesn\'t contain the item')); } catch (LocalizedException $e) { throw new GraphQlInputException(__($e->getMessage()), $e); } + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ 'model' => $cart, diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php index eb82510003fc7..55725e9fcce2b 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php @@ -69,6 +69,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); $this->checkCartCheckoutAllowance->execute($cart); $this->setBillingAddressOnCart->execute($context, $cart, $billingAddress); + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php index fb6c1e678f1f0..bc753d50db68a 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php @@ -69,6 +69,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); $this->checkCartCheckoutAllowance->execute($cart); $this->setPaymentMethodOnCart->execute($cart, $paymentData); + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php index d86244b2d8fc3..66bea8e886a57 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php @@ -69,6 +69,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); $this->checkCartCheckoutAllowance->execute($cart); $this->setShippingAddressesOnCart->execute($context, $cart, $shippingAddresses); + // reload updated cart + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php index e1cd9c18d9873..911078fd029f1 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php @@ -69,6 +69,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); $this->checkCartCheckoutAllowance->execute($cart); $this->setShippingMethodsOnCart->execute($context, $cart, $shippingMethods); + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php index 005baaad0e1e5..981f5f3603516 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php @@ -17,6 +17,7 @@ use Magento\Quote\Api\CartRepositoryInterface; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; use Magento\QuoteGraphQl\Model\CartItem\DataProvider\UpdateCartItems as UpdateCartItemsProvider; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; /** * @inheritdoc @@ -39,18 +40,26 @@ class UpdateCartItems implements ResolverInterface private $updateCartItems; /** - * @param GetCartForUser $getCartForUser + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + + /** + * @param GetCartForUser $getCartForUser * @param CartRepositoryInterface $cartRepository * @param UpdateCartItemsProvider $updateCartItems + * @param ArgumentsProcessorInterface $argsSelection */ public function __construct( GetCartForUser $getCartForUser, CartRepositoryInterface $cartRepository, - UpdateCartItemsProvider $updateCartItems + UpdateCartItemsProvider $updateCartItems, + ArgumentsProcessorInterface $argsSelection ) { $this->getCartForUser = $getCartForUser; $this->cartRepository = $cartRepository; $this->updateCartItems = $updateCartItems; + $this->argsSelection = $argsSelection; } /** @@ -58,19 +67,21 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (empty($args['input']['cart_id'])) { + $processedArgs = $this->argsSelection->process($info->fieldName, $args); + + if (empty($processedArgs['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); } - $maskedCartId = $args['input']['cart_id']; + $maskedCartId = $processedArgs['input']['cart_id']; - if (empty($args['input']['cart_items']) - || !is_array($args['input']['cart_items']) + if (empty($processedArgs['input']['cart_items']) + || !is_array($processedArgs['input']['cart_items']) ) { throw new GraphQlInputException(__('Required parameter "cart_items" is missing.')); } - $cartItems = $args['input']['cart_items']; + $cartItems = $processedArgs['input']['cart_items']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); @@ -83,6 +94,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value throw new GraphQlInputException(__($e->getMessage()), $e); } + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ 'model' => $cart, diff --git a/app/code/Magento/QuoteGraphQl/etc/di.xml b/app/code/Magento/QuoteGraphQl/etc/di.xml index 35b52dd495c5a..8dd35ab7f300b 100644 --- a/app/code/Magento/QuoteGraphQl/etc/di.xml +++ b/app/code/Magento/QuoteGraphQl/etc/di.xml @@ -33,4 +33,12 @@ + + + + Magento\QuoteGraphQl\Model\CartItem\CartItemUidArgsProcessor + Magento\QuoteGraphQl\Model\CartItem\CartItemsUidArgsProcessor + + + diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index cc9d1803b3e31..bcbbb3dc97de3 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -53,13 +53,13 @@ input CartItemInput { sku: String! quantity: Float! parent_sku: String @doc(description: "For child products, the SKU of its parent product") - selected_options: [ID!] @doc(description: "The selected options for the base product, such as color or size") + selected_options: [ID!] @doc(description: "The selected options for the base product, such as color or size with unique ID for a `CustomizableRadioOption`, `CustomizableDropDownOption`, `ConfigurableProductOptionsValues`, etc. objects") entered_options: [EnteredOptionInput!] @doc(description: "An array of entered options for the base product, such as personalization text") } input CustomizableOptionInput { - id: Int! - value_string: String! + id: Int @doc(description: "The customizable option id of the product") + value_string: String! @doc(description: "The string value of the option") } input ApplyCouponToCartInput { @@ -73,14 +73,16 @@ input UpdateCartItemsInput { } input CartItemUpdateInput { - cart_item_id: Int! + cart_item_id: Int @doc(description: "Deprecated. Use `cart_item_uid` instead.") + cart_item_uid: ID @doc(description: "The unique ID for a `CartItemInterface` object") quantity: Float customizable_options: [CustomizableOptionInput!] } input RemoveItemFromCartInput { cart_id: String! - cart_item_id: Int! + cart_item_id: Int @doc(description: "Deprecated. Use `cart_item_uid` instead.") + cart_item_uid: ID @doc(description: "Required field. The unique ID for a `CartItemInterface` object") } input SetShippingAddressesOnCartInput { @@ -199,9 +201,9 @@ type PlaceOrderOutput { } type Cart { - id: ID! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\MaskedCartId") @doc(description: "The ID of the cart.") + id: ID! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\MaskedCartId") @doc(description: "The unique ID for a `Cart` object") items: [CartItemInterface] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItems") - applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") @doc(description:"An array of coupons that have been applied to the cart") @deprecated(reason: "Use applied_coupons instead ") + applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") @doc(description:"An array of coupons that have been applied to the cart") @deprecated(reason: "Use applied_coupons instead") applied_coupons: [AppliedCoupon] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupons") @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code") email: String @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartEmail") shipping_addresses: [ShippingCartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddresses") @@ -328,7 +330,8 @@ type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Car } interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolver") { - id: String! + id: String! @deprecated(reason: "Use `uid` instead") + uid: ID! @doc(description: "The unique ID for a `CartItemInterface` object") quantity: Float! prices: CartItemPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") product: ProductInterface! @@ -348,7 +351,8 @@ type CartItemPrices { } type SelectedCustomizableOption { - id: Int! + id: Int! @deprecated(reason: "Use SelectedCustomizableOption.customizable_option_uid instead") + customizable_option_uid: ID! @doc(description: "The unique ID for a `CustomizableRadioOption`, `CustomizableDropDownOption`, `CustomizableMultipleOption`, etc. of `CustomizableOptionInterface` objects") label: String! is_required: Boolean! values: [SelectedCustomizableOptionValue!]! @@ -356,7 +360,8 @@ type SelectedCustomizableOption { } type SelectedCustomizableOptionValue { - id: Int! + id: Int! @deprecated(reason: "Use SelectedCustomizableOptionValue.customizable_option_value_uid instead") + customizable_option_value_uid: ID! @doc(description: "The unique ID for a `CustomizableMultipleValue`, `CustomizableRadioValue`, `CustomizableCheckboxValue`, `CustomizableDropDownValue`, etc. objects") label: String! value: String! price: CartItemSelectedOptionValuePrice! @@ -369,7 +374,7 @@ type CartItemSelectedOptionValuePrice { } type Order { - order_number: String! + order_number: String! @doc(description: "The unique ID for a `Order` object.") order_id: String @deprecated(reason: "The order_id field is deprecated, use order_number instead.") } diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls index 3544acd1564d0..8a76f51d78e79 100644 --- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -39,7 +39,7 @@ type CustomerOrders @doc(description: "The collection of orders that match the c } type CustomerOrder @doc(description: "Contains details about each of the customer's orders") { - id: ID! @doc(description: "Unique identifier for the order") + id: ID! @doc(description: "The unique ID for a `CustomerOrder` object") order_date: String! @doc(description: "The date the order was placed") status: String! @doc(description: "The current status of the order") number: String! @doc(description: "The order number") @@ -65,7 +65,7 @@ type OrderAddress @doc(description: "OrderAddress contains detailed information lastname: String! @doc(description: "The family name of the person associated with the shipping/billing address") middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") region: String @doc(description: "The state or province name") - region_id: ID @doc(description: "The unique ID for a pre-defined region") + region_id: ID @doc(description: "The unique ID for a `Region` object of a pre-defined region") country_code: CountryCodeEnum @doc(description: "The customer's country") street: [String!]! @doc(description: "An array of strings that define the street number and name") company: String @doc(description: "The customer's company") @@ -79,7 +79,7 @@ type OrderAddress @doc(description: "OrderAddress contains detailed information } interface OrderItemInterface @doc(description: "Order item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\OrderItem") { - id: ID! @doc(description: "The unique identifier of the order item") + id: ID! @doc(description: "The unique ID for a `OrderItemInterface` object") product_name: String @doc(description: "The name of the base product") product_sku: String! @doc(description: "The SKU of the base product") product_url_key: String @doc(description: "URL key of the base product") @@ -123,7 +123,7 @@ type OrderTotal @doc(description: "Contains details about the sales total amount } type Invoice @doc(description: "Invoice details") { - id: ID! @doc(description: "The ID of the invoice, used for API purposes") + id: ID! @doc(description: "The unique ID for a `Invoice` object") number: String! @doc(description: "Sequential invoice number") total: InvoiceTotal @doc(description: "Invoice total amount details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoice\\InvoiceTotal") items: [InvoiceItemInterface] @doc(description: "Invoiced product details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoice\\InvoiceItems") @@ -131,7 +131,7 @@ type Invoice @doc(description: "Invoice details") { } interface InvoiceItemInterface @doc(description: "Invoice item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\InvoiceItem") { - id: ID! @doc(description: "The unique ID of the invoice item") + id: ID! @doc(description: "The unique ID for a `InvoiceItemInterface` object") order_item: OrderItemInterface @doc(description: "Contains details about an individual order item") product_name: String @doc(description: "The name of the base product") product_sku: String! @doc(description: "The SKU of the base product") @@ -167,7 +167,7 @@ type ShippingDiscount @doc(description:"Defines an individual shipping discount. } type OrderShipment @doc(description: "Order shipment details") { - id: ID! @doc(description: "The unique ID of the shipment") + id: ID! @doc(description: "The unique ID for a `OrderShipment` object") number: String! @doc(description: "The sequential credit shipment number") tracking: [ShipmentTracking] @doc(description: "Contains shipment tracking details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Shipment\\ShipmentTracking") items: [ShipmentItemInterface] @doc(description: "Contains items included in the shipment") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Shipment\\ShipmentItems") @@ -180,7 +180,7 @@ type SalesCommentItem @doc(description: "Comment item details") { } interface ShipmentItemInterface @doc(description: "Order shipment item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\ShipmentItem"){ - id: ID! @doc(description: "Shipment item unique identifier") + id: ID! @doc(description: "The unique ID for a `ShipmentItemInterface` object") order_item: OrderItemInterface @doc(description: "Associated order item") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItem") product_name: String @doc(description: "Name of the base product") product_sku: String! @doc(description: "SKU of the base product") @@ -204,7 +204,7 @@ type OrderPaymentMethod @doc(description: "Contains details about the payment me } type CreditMemo @doc(description: "Credit memo details") { - id: ID! @doc(description: "The unique ID of the credit memo, used for API purposes") + id: ID! @doc(description: "The unique ID for a `CreditMemo` object") number: String! @doc(description: "The sequential credit memo number") items: [CreditMemoItemInterface] @doc(description: "An array containing details about refunded items") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemo\\CreditMemoItems") total: CreditMemoTotal @doc(description: "Contains details about the total refunded amount") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemo\\CreditMemoTotal") @@ -212,7 +212,7 @@ type CreditMemo @doc(description: "Credit memo details") { } interface CreditMemoItemInterface @doc(description: "Credit memo item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\CreditMemoItem") { - id: ID! @doc(description: "The unique ID of the credit memo item, used for API purposes") + id: ID! @doc(description: "The unique ID for a `CreditMemoItemInterface` object") order_item: OrderItemInterface @doc(description: "The order item the credit memo is applied to") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItem") product_name: String @doc(description: "The name of the base product") product_sku: String! @doc(description: "SKU of the base product") diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php index 6430f71765fe4..0b372555fe4c2 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -9,6 +9,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -36,16 +37,24 @@ class EntityUrl implements ResolverInterface */ private $redirectType; + /** + * @var Uid + */ + private $idEncoder; + /** * @param UrlFinderInterface $urlFinder * @param CustomUrlLocatorInterface $customUrlLocator + * @param Uid $idEncoder */ public function __construct( UrlFinderInterface $urlFinder, - CustomUrlLocatorInterface $customUrlLocator + CustomUrlLocatorInterface $customUrlLocator, + Uid $idEncoder ) { $this->urlFinder = $urlFinder; $this->customUrlLocator = $customUrlLocator; + $this->idEncoder = $idEncoder; } /** @@ -78,6 +87,7 @@ public function resolve( $relativeUrl = $finalUrlRewrite->getRequestPath(); $resultArray = $this->rewriteCustomUrls($finalUrlRewrite, $storeId) ?? [ 'id' => $finalUrlRewrite->getEntityId(), + 'entity_uid' => $this->idEncoder->encode((string)$finalUrlRewrite->getEntityId()), 'canonical_url' => $relativeUrl, 'relative_url' => $relativeUrl, 'redirectCode' => $this->redirectType, @@ -115,6 +125,7 @@ private function rewriteCustomUrls(UrlRewrite $finalUrlRewrite, int $storeId): ? ? $finalCustomUrlRewrite->getRequestPath() : $finalUrlRewrite->getRequestPath(); return [ 'id' => $finalUrlRewrite->getEntityId(), + 'entity_uid' => $this->idEncoder->encode((string)$finalUrlRewrite->getEntityId()), 'canonical_url' => $relativeUrl, 'relative_url' => $relativeUrl, 'redirectCode' => $finalCustomUrlRewrite->getRedirectType(), diff --git a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls index 7f7ebb627b4dc..7000f52d7d683 100644 --- a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls @@ -6,7 +6,8 @@ type Query { } type EntityUrl @doc(description: "EntityUrl is an output object containing the `id`, `relative_url`, and `type` attributes") { - id: Int @doc(description: "The ID assigned to the object associated with the specified url. This could be a product ID, category ID, or page ID.") + id: Int @deprecated(reason: "Use `entity_uid` instead.") @doc(description: "The ID assigned to the object associated with the specified url. This could be a product ID, category ID, or page ID.") + entity_uid: ID @doc(description: "The unique ID for a `ProductInterface`, `CategoryInterface`, `CmsPage`, etc. object associated with the specified url. This could be a product UID, category UID, or cms page UID.") canonical_url: String @deprecated(reason: "The canonical_url field is deprecated, use relative_url instead.") relative_url: String @doc(description: "The internal relative URL. If the specified url is a redirect, the query returns the redirected URL, not the original.") redirectCode: Int @doc(description: "301 or 302 HTTP code for url permanent or temporary redirect or 0 for the 200 no redirect") diff --git a/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php index 9cc1404613e41..96e39b6566d5d 100644 --- a/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php +++ b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php @@ -7,6 +7,7 @@ namespace Magento\WishlistGraphQl\Mapper; +use Magento\Framework\GraphQl\Schema\Type\Enum\DataMapperInterface; use Magento\Wishlist\Model\Wishlist; /** @@ -14,6 +15,20 @@ */ class WishlistDataMapper { + /** + * @var DataMapperInterface + */ + private $enumDataMapper; + + /** + * @param DataMapperInterface $enumDataMapper + */ + public function __construct( + DataMapperInterface $enumDataMapper + ) { + $this->enumDataMapper = $enumDataMapper; + } + /** * Mapping the review data * @@ -29,7 +44,26 @@ public function map(Wishlist $wishlist): array 'updated_at' => $wishlist->getUpdatedAt(), 'items_count' => $wishlist->getItemsCount(), 'name' => $wishlist->getName(), + 'visibility' => $this->getMappedVisibility((int) $wishlist->getVisibility()), 'model' => $wishlist, ]; } + + /** + * Get wishlist mapped visibility + * + * @param int $visibility + * + * @return string|null + */ + private function getMappedVisibility(int $visibility): ?string + { + if ($visibility === null) { + return null; + } + + $visibilityEnums = $this->enumDataMapper->getMappedEnums('WishlistVisibilityEnum'); + + return isset($visibilityEnums[$visibility]) ? strtoupper($visibilityEnums[$visibility]) : null; + } } diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItems.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItems.php index 77ff483a60bd2..bf9fe1875c228 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItems.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItems.php @@ -61,7 +61,9 @@ public function resolve( /** @var Wishlist $wishlist */ $wishlist = $value['model']; - $wishlistItems = $this->getWishListItems($wishlist); + /** @var WishlistItemCollection $wishlistItemCollection */ + $wishlistItemsCollection = $this->getWishListItems($wishlist, $args); + $wishlistItems = $wishlistItemsCollection->getItems(); $data = []; foreach ($wishlistItems as $wishlistItem) { @@ -74,17 +76,28 @@ public function resolve( 'itemModel' => $wishlistItem, ]; } - return $data; + return [ + 'items' => $data, + 'page_info' => [ + 'current_page' => $wishlistItemsCollection->getCurPage(), + 'page_size' => $wishlistItemsCollection->getPageSize(), + 'total_pages' => $wishlistItemsCollection->getLastPageNumber() + ] + ]; } /** * Get wishlist items * * @param Wishlist $wishlist - * @return Item[] + * @param array $args + * @return WishlistItemCollection */ - private function getWishListItems(Wishlist $wishlist): array + private function getWishListItems(Wishlist $wishlist, array $args): WishlistItemCollection { + $currentPage = $args['currentPage'] ?? 1; + $pageSize = $args['pageSize'] ?? 20; + /** @var WishlistItemCollection $wishlistItemCollection */ $wishlistItemCollection = $this->wishlistItemCollectionFactory->create(); $wishlistItemCollection @@ -93,6 +106,13 @@ private function getWishListItems(Wishlist $wishlist): array return $store->getId(); }, $this->storeManager->getStores())) ->setVisibilityFilter(); - return $wishlistItemCollection->getItems(); + if ($currentPage > 0) { + $wishlistItemCollection->setCurPage($currentPage); + } + + if ($pageSize > 0) { + $wishlistItemCollection->setPageSize($pageSize); + } + return $wishlistItemCollection; } } diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index 7812176db60d0..5a44facb606ac 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -11,7 +11,7 @@ type Customer { currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1.") ): [Wishlist!]! @doc(description: "An array of wishlists. In Magento Open Source, customers are limited to one wish list. The number of wish lists is configurable for Magento Commerce") @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlists") wishlist: Wishlist! @deprecated(reason: "Use `Customer.wishlists` or `Customer.wishlist_v2`") @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "Contains a customer's wish lists") @cache(cacheable: false) - wishlist_v2(id: ID!): Wishlist @doc(description: "Retrieve the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistById") + wishlist_v2(id: ID!): Wishlist @doc(description: "Retrieve the specified wish list identified by the unique ID for a `Wishlist` object") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistById") } type WishlistOutput @doc(description: "Deprecated: `Wishlist` type should be used instead") { @@ -23,16 +23,19 @@ type WishlistOutput @doc(description: "Deprecated: `Wishlist` type should be use } type Wishlist { - id: ID @doc(description: "Wishlist unique identifier") + id: ID @doc(description: "The unique ID for a `Wishlist` object") items: [WishlistItem] @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistItemsResolver") @deprecated(reason: "Use field `items_v2` from type `Wishlist` instead") - items_v2: [WishlistItemInterface] @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistItems") @doc(description: "An array of items in the customer's wish list") + items_v2( + currentPage: Int = 1, + pageSize: Int = 20 + ): WishlistItems @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistItems") @doc(description: "An array of items in the customer's wish list") items_count: Int @doc(description: "The number of items in the wish list") sharing_code: String @doc(description: "An encrypted code that Magento uses to link to the wish list") updated_at: String @doc(description: "The time of the last modification to the wish list") } interface WishlistItemInterface @typeResolver(class: "Magento\\WishlistGraphQl\\Model\\Resolver\\Type\\WishlistItemType") { - id: ID! @doc(description: "The ID of the wish list item") + id: ID! @doc(description: "The unique ID for a `WishlistItemInterface` object") quantity: Float! @doc(description: "The quantity of this wish list item") description: String @doc(description: "The description of the item") added_at: String! @doc(description: "The date and time the item was added to the wish list") @@ -40,8 +43,13 @@ interface WishlistItemInterface @typeResolver(class: "Magento\\WishlistGraphQl\\ customizable_options: [SelectedCustomizableOption] @doc(description: "Custom options selected for the wish list item") } +type WishlistItems { + items: [WishlistItemInterface]! @doc(description: "A list of items in the wish list") + page_info: SearchResultPageInfo @doc(description: "Contains pagination metadata") +} + type WishlistItem { - id: Int @doc(description: "The wish list item ID") + id: Int @doc(description: "The unique ID for a `WishlistItem` object") qty: Float @doc(description: "The quantity of this wish list item"), description: String @doc(description: "The customer's comment about this item"), added_at: String @doc(description: "The time when the customer added the item to the wish list"), @@ -73,7 +81,7 @@ type RemoveProductsFromWishlistOutput @doc(description: "Contains the customer's } input WishlistItemUpdateInput @doc(description: "Defines updates to items in a wish list") { - wishlist_item_id: ID! @doc(description: "The ID of the wishlist item to update") + wishlist_item_id: ID! @doc(description: "The unique ID for a `WishlistItemInterface` object") quantity: Float @doc(description: "The new amount or number of this item") description: String @doc(description: "Customer-entered comments about the item") selected_options: [ID!] @doc(description: "An array of strings corresponding to options the customer selected") diff --git a/composer.json b/composer.json index e4ed67b727912..6aa9355cec7b1 100644 --- a/composer.json +++ b/composer.json @@ -139,6 +139,7 @@ "magento/module-checkout-agreements-graph-ql": "*", "magento/module-cms": "*", "magento/module-cms-url-rewrite": "*", + "magento/module-compare-list-graph-ql": "*", "magento/module-config": "*", "magento/module-configurable-import-export": "*", "magento/module-configurable-product": "*", @@ -199,6 +200,7 @@ "magento/module-login-as-customer-api": "*", "magento/module-login-as-customer-assistance": "*", "magento/module-login-as-customer-frontend-ui": "*", + "magento/module-login-as-customer-graph-ql": "*", "magento/module-login-as-customer-log": "*", "magento/module-login-as-customer-quote": "*", "magento/module-login-as-customer-page-cache": "*", diff --git a/composer.lock b/composer.lock index f4ece70a22e62..fa90484975728 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "50fd3418a729ef9b577d214fe6c9b0b1", + "content-hash": "ac6fc13ba98a815bce589d300d28012c", "packages": [ { "name": "aws/aws-sdk-php", @@ -1459,12 +1459,6 @@ "BSD-3-Clause" ], "description": "Replace zendframework and zfcampus packages with their Laminas Project equivalents.", - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-05-20T13:45:39+00:00" }, { @@ -3720,13 +3714,11 @@ "Magento\\Composer\\": "src" } }, - "notification-url": "https://packagist.org/downloads/", "license": [ "OSL-3.0", "AFL-3.0" ], - "description": "Magento composer library helps to instantiate Composer application and run composer commands.", - "time": "2020-06-15T17:52:31+00:00" + "description": "Magento composer library helps to instantiate Composer application and run composer commands." }, { "name": "magento/magento-composer-installer", @@ -7556,20 +7548,6 @@ "parser", "php" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], "time": "2020-05-25T17:44:05+00:00" }, { @@ -9560,20 +9538,6 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", - "funding": [ - { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" - } - ], "time": "2020-05-05T12:55:44+00:00" }, { @@ -9863,12 +9827,6 @@ "keywords": [ "timer" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-04-20T06:00:37+00:00" }, { @@ -10013,16 +9971,6 @@ "testing", "xunit" ], - "funding": [ - { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-05-22T13:54:05+00:00" }, { @@ -12059,5 +12007,6 @@ "ext-zip": "*", "lib-libxml": "*" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "1.1.0" } diff --git a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls index 1a5796e07b08b..85684ead2532e 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls +++ b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls @@ -2,20 +2,22 @@ # See COPYING.txt for license details. type Query { - testItem(id: Int!) : Item @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\Item") + testItem(id: Int!) : TestItemOutput @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\Item") testUnion: TestUnion @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\TestUnion") + testQueryWithNestedMandatoryInputArguments(input: TestInputQueryWithMandatoryArgumentsInput): TestItemOutput + testQueryWithTopLevelMandatoryInputArguments(topLevelArgument: String!): TestItemOutput } type Mutation { - testItem(id: Int!) : MutationItem @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\Item") + testItem(id: Int!) : MutationItemOutput @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\Item") } -type Item { +type TestItemOutput { item_id: Int name: String } -type MutationItem { +type MutationItemOutput { item_id: Int name: String } @@ -30,3 +32,13 @@ type TypeCustom1 { type TypeCustom2 { custom_name2: String } + +input TestInputQueryWithMandatoryArgumentsInput { + query_id: String! + query_items: [QueryWithMandatoryArgumentsInput!]! +} + +input QueryWithMandatoryArgumentsInput { + query_item_id: Int! + quantity: Float +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/etc/schema.graphqls b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/etc/schema.graphqls index b970ad8376349..acf1f20e3c006 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/etc/schema.graphqls +++ b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/etc/schema.graphqls @@ -1,10 +1,10 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. -type Item { +type TestItemOutput { integer_list: [Int] @resolver(class: "Magento\\TestModuleGraphQlQueryExtension\\Model\\Resolver\\IntegerList") } -type MutationItem { +type MutationItemOutput { integer_list: [Int] @resolver(class: "Magento\\TestModuleGraphQlQueryExtension\\Model\\Resolver\\IntegerList") } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php index 3ef6e6618c6c5..9e34027db6aea 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php @@ -35,7 +35,8 @@ class ApiConfigFixture extends ConfigFixture protected function setStoreConfigValue(array $matches, $configPathAndValue): void { $storeCode = $matches[0]; - [$configScope, $configPath, $requiredValue] = preg_split('/\s+/', $configPathAndValue, 3); + $parts = preg_split('/\s+/', $configPathAndValue, 3); + [$configScope, $configPath, $requiredValue] = $parts + ['', '', '']; /** @var ConfigStorage $configStorage */ $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); if (!$configStorage->checkIsRecordExist($configPath, ScopeInterface::SCOPE_STORES, $storeCode)) { @@ -69,7 +70,8 @@ protected function setGlobalConfigValue($configPathAndValue): void protected function setWebsiteConfigValue(array $matches, $configPathAndValue): void { $websiteCode = $matches[0]; - [$configScope, $configPath, $requiredValue] = preg_split('/\s+/', $configPathAndValue, 3); + $parts = preg_split('/\s+/', $configPathAndValue, 3); + [$configScope, $configPath, $requiredValue] = $parts + ['', '', '']; /** @var ConfigStorage $configStorage */ $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); if (!$configStorage->checkIsRecordExist($configPath, ScopeInterface::SCOPE_WEBSITES, $websiteCode)) { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php index fc0fdcf71525f..cb62cc0f75264 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php @@ -97,6 +97,7 @@ public function testAddBundleProductToCart() cart { items { id + uid quantity product { sku @@ -104,10 +105,12 @@ public function testAddBundleProductToCart() ... on BundleCartItem { bundle_options { id + uid label type values { id + uid label price quantity @@ -189,6 +192,7 @@ public function testAddBundleToCartWithWrongBundleOptions() cart { items { id + uid quantity product { sku @@ -196,10 +200,12 @@ public function testAddBundleToCartWithWrongBundleOptions() ... on BundleCartItem { bundle_options { id + uid label type values { id + uid label price quantity @@ -268,6 +274,7 @@ private function getProductQuery(string $sku): string items { sku option_id + uid required type title @@ -279,8 +286,8 @@ private function getProductQuery(string $sku): string } can_change_quantity id + uid price - quantity } } @@ -322,6 +329,7 @@ private function getMutationsQuery( cart { items { id + uid quantity product { sku @@ -329,10 +337,12 @@ private function getMutationsQuery( ... on BundleCartItem { bundle_options { id + uid label type values { id + uid label price quantity diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php index f705195050843..2c6d3af69bf40 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php @@ -104,6 +104,7 @@ public function testAddBundleProductToCart() cart { items { id + uid quantity product { sku @@ -111,10 +112,12 @@ public function testAddBundleProductToCart() ... on BundleCartItem { bundle_options { id + uid label type values { id + uid label price quantity diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php index a18b6e1206895..bc3fcd3a8427b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php @@ -38,7 +38,6 @@ public function testAllFieldsBundleProducts() type_id id name - attribute_set_id ... on PhysicalProductInterface { weight } @@ -54,7 +53,7 @@ public function testAllFieldsBundleProducts() required type position - sku + sku options { id quantity @@ -74,7 +73,7 @@ public function testAllFieldsBundleProducts() } } } - } + } } QUERY; @@ -118,7 +117,6 @@ public function testBundleProductWithNotVisibleChildren() type_id id name - attribute_set_id ... on PhysicalProductInterface { weight } @@ -134,7 +132,7 @@ public function testBundleProductWithNotVisibleChildren() required type position - sku + sku options { id quantity @@ -154,7 +152,7 @@ public function testBundleProductWithNotVisibleChildren() } } } - } + } } QUERY; @@ -207,8 +205,7 @@ private function assertBundleBaseFields($product, $actualResponse) ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], ['response_field' => 'id', 'expected_value' => $product->getId()], ['response_field' => 'name', 'expected_value' => $product->getName()], - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'weight', 'expected_value' => $product->getWeight()], + ['response_field' => 'weight', 'expected_value' => $product->getWeight()], ['response_field' => 'dynamic_price', 'expected_value' => !(bool)$product->getPriceType()], ['response_field' => 'dynamic_weight', 'expected_value' => !(bool)$product->getWeightType()], ['response_field' => 'dynamic_sku', 'expected_value' => !(bool)$product->getSkuType()] diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php index 01777cfbfd694..18f21bcbc2ee5 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php @@ -32,6 +32,7 @@ public function testFilterSingleCategoryByField($field, $condition, $value, $exp categories(filters: { $field : { $condition : "$value" } }){ items{ id + uid name url_key url_path @@ -63,6 +64,7 @@ public function testFilterMultipleCategoriesByField($field, $condition, $value, categories(filters: { $field : { $condition : $value } }){ items{ id + uid name url_key url_path @@ -92,6 +94,7 @@ public function testFilterCategoryByMultipleFields() total_count items{ id + uid name url_key url_path @@ -122,6 +125,7 @@ public function testFilterWithInactiveCategory() categories(filters: {url_key: {in: ["inactive", "category-2"]}}){ items{ id + uid name url_key url_path @@ -147,6 +151,7 @@ public function testQueryChildCategoriesWithProducts() categories(filters: {ids: {in: ["3"]}}){ items{ id + uid name url_key url_path @@ -233,6 +238,7 @@ public function testQueryCategoryWithDisabledChildren() categories(filters: {ids: {in: ["3"]}}){ items{ id + uid name image url_key @@ -315,6 +321,7 @@ public function testNoResultsFound() categories(filters: {url_key: {in: ["inactive", "does-not-exist"]}}){ items{ id + uid name url_key url_path @@ -343,6 +350,7 @@ public function testEmptyFiltersReturnRootCategory() categories{ items{ id + uid name url_key url_path @@ -378,6 +386,7 @@ public function testMinimumMatchQueryLength() categories(filters: {name: {match: "mo"}}){ items{ id + uid name url_key url_path @@ -412,6 +421,7 @@ public function testCategoryImageNameAndSeoDisabled() categories(filters: {ids: {in: ["$categoryId"]}}) { items{ id + uid name image } @@ -444,6 +454,7 @@ public function testFilterByUrlPathTopLevelCategory() categories(filters: {url_path: {eq: "$urlPath"}}){ items{ id + uid name url_key url_path @@ -473,6 +484,7 @@ public function testFilterByUrlPathNestedCategory() categories(filters: {url_path: {eq: "$urlPath"}}){ items{ id + uid name url_key url_path @@ -503,6 +515,7 @@ public function testFilterByUrlPathMultipleCategories() categories(filters: {url_path: {in: [$urlPathsString]}}){ items{ id + uid name url_key url_path @@ -533,6 +546,7 @@ public function testFilterByUrlPathNoResults() categories(filters: {url_path: {in: ["not-a-category url path"]}}){ items{ id + uid name url_key url_path @@ -561,6 +575,22 @@ public function filterSingleCategoryDataProvider(): array '4', [ 'id' => '4', + 'uid' => base64_encode('4'), + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ] + ], + [ + 'category_uid', + 'eq', + base64_encode('4'), + [ + 'id' => '4', + 'uid' => base64_encode('4'), 'name' => 'Category 1.1', 'url_key' => 'category-1-1', 'url_path' => 'category-1/category-1-1', @@ -589,6 +619,7 @@ public function filterSingleCategoryDataProvider(): array 'Movable Position 2', [ 'id' => '10', + 'uid' => base64_encode('10'), 'name' => 'Movable Position 2', 'url_key' => 'movable-position-2', 'url_path' => 'movable-position-2', @@ -603,6 +634,7 @@ public function filterSingleCategoryDataProvider(): array 'category-1-1-1', [ 'id' => '5', + 'uid' => base64_encode('5'), 'name' => 'Category 1.1.1', 'url_key' => 'category-1-1-1', 'url_path' => 'category-1/category-1-1/category-1-1-1', @@ -656,6 +688,44 @@ public function filterMultipleCategoriesDataProvider(): array ] ] ], + //Filter by multiple UIDs + [ + 'category_uid', + 'in', + '["' . base64_encode('4') . '", "' . base64_encode('9') . '", "' . base64_encode('10') . '"]', + [ + [ + 'id' => '4', + 'uid' => base64_encode('4'), + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ], + [ + 'id' => '9', + 'uid' => base64_encode('9'), + 'name' => 'Movable Position 1', + 'url_key' => 'movable-position-1', + 'url_path' => 'movable-position-1', + 'children_count' => '0', + 'path' => '1/2/9', + 'position' => '5' + ], + [ + 'id' => '10', + 'uid' => base64_encode('10'), + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ] + ] + ], // Filter by multiple parent IDs [ 'parent_id', @@ -699,6 +769,7 @@ public function filterMultipleCategoriesDataProvider(): array [ [ 'id' => '13', + 'uid' => base64_encode('13'), 'name' => 'Category 1.2', 'url_key' => 'category-1-2', 'url_path' => 'category-1/category-1-2', @@ -708,6 +779,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '7', + 'uid' => base64_encode('7'), 'name' => 'Movable', 'url_key' => 'movable', 'url_path' => 'movable', @@ -725,6 +797,7 @@ public function filterMultipleCategoriesDataProvider(): array [ [ 'id' => '9', + 'uid' => base64_encode('9'), 'name' => 'Movable Position 1', 'url_key' => 'movable-position-1', 'url_path' => 'movable-position-1', @@ -734,6 +807,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '10', + 'uid' => base64_encode('10'), 'name' => 'Movable Position 2', 'url_key' => 'movable-position-2', 'url_path' => 'movable-position-2', @@ -743,6 +817,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '11', + 'uid' => base64_encode('11'), 'name' => 'Movable Position 3', 'url_key' => 'movable-position-3', 'url_path' => 'movable-position-3', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php index dbbeaebc15936..8a483b5b12605 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php @@ -308,9 +308,7 @@ public function testCategoryProducts() page_size } items { - attribute_set_id country_of_manufacture - created_at description { html } @@ -349,8 +347,6 @@ public function testCategoryProducts() } } name - new_from_date - new_to_date options_container price { minimalPrice { @@ -409,7 +405,6 @@ public function testCategoryProducts() sku small_image { url, label } thumbnail { url, label } - special_from_date special_price special_to_date swatch_image @@ -422,17 +417,8 @@ public function testCategoryProducts() website_id } type_id - updated_at url_key url_path - websites { - id - name - code - sort_order - default_group_id - is_default - } } } } @@ -453,7 +439,6 @@ public function testCategoryProducts() $firstProductModel = $productRepository->get($firstProduct['sku'], false, null, true); $this->assertBaseFields($firstProductModel, $firstProduct); $this->assertAttributes($firstProduct); - $this->assertWebsites($firstProductModel, $firstProduct['websites']); $this->assertEquals('Category 1', $firstProduct['categories'][0]['name']); $this->assertEquals('category-1/category-1-1', $firstProduct['categories'][1]['url_path']); $this->assertCount(3, $firstProduct['categories']); @@ -636,6 +621,37 @@ public function testCategoryImage(?string $imagePrefix) $this->assertEquals($expectedImageUrl, $childCategory['image']); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testGetCategoryWithIdAndUid() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('`ids` and `category_uid` can\'t be used at the same time'); + + $categoryId = 8; + $categoryUid = base64_encode((string) 8); + $query = <<graphQlQuery($query); + } + /** * @return array */ @@ -664,8 +680,6 @@ public function categoryImageDataProvider(): array private function assertBaseFields($product, $actualResponse) { $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], ['response_field' => 'name', 'expected_value' => $product->getName()], ['response_field' => 'price', 'expected_value' => [ 'minimalPrice' => [ @@ -693,30 +707,10 @@ private function assertBaseFields($product, $actualResponse) ], ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], - ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ]; $this->assertResponseFields($actualResponse, $assertionMap); } - /** - * @param ProductInterface $product - * @param array $actualResponse - */ - private function assertWebsites($product, $actualResponse) - { - $assertionMap = [ - [ - 'id' => current($product->getExtensionAttributes()->getWebsiteIds()), - 'name' => 'Main Website', - 'code' => 'base', - 'sort_order' => 0, - 'default_group_id' => '1', - 'is_default' => true, - ] - ]; - $this->assertEquals($actualResponse, $assertionMap); - } - /** * @param array $actualResponse */ @@ -731,11 +725,8 @@ private function assertAttributes($actualResponse) 'short_description', 'country_of_manufacture', 'gift_message_available', - 'new_from_date', - 'new_to_date', 'options_container', 'special_price', - 'special_from_date', 'special_to_date', ]; foreach ($eavAttributes as $eavAttribute) { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php index 43612575a7dcb..7cb7dacf2b188 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -42,6 +42,7 @@ public function testFilterSingleCategoryByField($field, $condition, $value, $exp { categoryList(filters: { $field : { $condition : "$value" } }){ id + uid name url_key url_path @@ -71,6 +72,7 @@ public function testFilterMultipleCategoriesByField($field, $condition, $value, { categoryList(filters: { $field : { $condition : $value } }){ id + uid name url_key url_path @@ -337,6 +339,7 @@ public function testEmptyFiltersReturnRootCategory() { categoryList{ id + uid name url_key url_path @@ -354,6 +357,7 @@ public function testEmptyFiltersReturnRootCategory() $this->assertArrayHasKey('categoryList', $result); $this->assertEquals('Default Category', $result['categoryList'][0]['name']); $this->assertEquals($storeRootCategoryId, $result['categoryList'][0]['id']); + $this->assertEquals(base64_encode($storeRootCategoryId), $result['categoryList'][0]['uid']); } /** @@ -370,6 +374,7 @@ public function testMinimumMatchQueryLength() { categoryList(filters: {name: {match: "mo"}}){ id + uid name url_key url_path @@ -542,6 +547,22 @@ public function filterSingleCategoryDataProvider(): array '4', [ 'id' => '4', + 'uid' => base64_encode('4'), + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ] + ], + [ + 'category_uid', + 'eq', + base64_encode('4'), + [ + 'id' => '4', + 'uid' => base64_encode('4'), 'name' => 'Category 1.1', 'url_key' => 'category-1-1', 'url_path' => 'category-1/category-1-1', @@ -556,6 +577,7 @@ public function filterSingleCategoryDataProvider(): array 'Movable Position 2', [ 'id' => '10', + 'uid' => base64_encode('10'), 'name' => 'Movable Position 2', 'url_key' => 'movable-position-2', 'url_path' => 'movable-position-2', @@ -596,6 +618,45 @@ public function filterMultipleCategoriesDataProvider(): array [ [ 'id' => '4', + 'uid' => base64_encode('4'), + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ], + [ + 'id' => '9', + 'uid' => base64_encode('9'), + 'name' => 'Movable Position 1', + 'url_key' => 'movable-position-1', + 'url_path' => 'movable-position-1', + 'children_count' => '0', + 'path' => '1/2/9', + 'position' => '5' + ], + [ + 'id' => '10', + 'uid' => base64_encode('10'), + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ] + ] + ], + //Filter by multiple UIDs + [ + 'category_uid', + 'in', + '["' . base64_encode('4') . '", "' . base64_encode('9') . '", "' . base64_encode('10') . '"]', + [ + [ + 'id' => '4', + 'uid' => base64_encode('4'), 'name' => 'Category 1.1', 'url_key' => 'category-1-1', 'url_path' => 'category-1/category-1-1', @@ -605,6 +666,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '9', + 'uid' => base64_encode('9'), 'name' => 'Movable Position 1', 'url_key' => 'movable-position-1', 'url_path' => 'movable-position-1', @@ -614,6 +676,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '10', + 'uid' => base64_encode('10'), 'name' => 'Movable Position 2', 'url_key' => 'movable-position-2', 'url_path' => 'movable-position-2', @@ -631,6 +694,7 @@ public function filterMultipleCategoriesDataProvider(): array [ [ 'id' => '13', + 'uid' => base64_encode('13'), 'name' => 'Category 1.2', 'url_key' => 'category-1-2', 'url_path' => 'category-1/category-1-2', @@ -640,6 +704,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '7', + 'uid' => base64_encode('7'), 'name' => 'Movable', 'url_key' => 'movable', 'url_path' => 'movable', @@ -657,6 +722,7 @@ public function filterMultipleCategoriesDataProvider(): array [ [ 'id' => '9', + 'uid' => base64_encode('9'), 'name' => 'Movable Position 1', 'url_key' => 'movable-position-1', 'url_path' => 'movable-position-1', @@ -666,6 +732,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '10', + 'uid' => base64_encode('10'), 'name' => 'Movable Position 2', 'url_key' => 'movable-position-2', 'url_path' => 'movable-position-2', @@ -675,6 +742,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '11', + 'uid' => base64_encode('11'), 'name' => 'Movable Position 3', 'url_key' => 'movable-position-3', 'url_path' => 'movable-position-3', @@ -725,6 +793,7 @@ public function testFilterCategoryInlineFragment() categoryList(filters: {ids: {eq: "6"}}){ ... on CategoryTree { id + uid name url_key url_path @@ -739,6 +808,7 @@ public function testFilterCategoryInlineFragment() $this->assertArrayNotHasKey('errors', $result); $this->assertCount(1, $result['categoryList']); $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['uid'], base64_encode('6')); $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); } @@ -756,6 +826,7 @@ public function testFilterCategoryNamedFragment() fragment Cat on CategoryTree { id + uid name url_key url_path @@ -768,6 +839,7 @@ public function testFilterCategoryNamedFragment() $this->assertArrayNotHasKey('errors', $result); $this->assertCount(1, $result['categoryList']); $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['uid'], base64_encode('6')); $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index 72b014fd39f0e..a77eccf5c623f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -333,9 +333,7 @@ public function testCategoryProducts() page_size } items { - attribute_set_id country_of_manufacture - created_at description { html } @@ -374,8 +372,6 @@ public function testCategoryProducts() } } name - new_from_date - new_to_date options_container price { minimalPrice { @@ -434,7 +430,6 @@ public function testCategoryProducts() sku small_image { url, label } thumbnail { url, label } - special_from_date special_price special_to_date swatch_image @@ -447,17 +442,8 @@ public function testCategoryProducts() website_id } type_id - updated_at url_key url_path - websites { - id - name - code - sort_order - default_group_id - is_default - } } } } @@ -478,7 +464,6 @@ public function testCategoryProducts() $firstProduct = $productRepository->get($firstProductSku, false, null, true); $this->assertBaseFields($firstProduct, $response['category']['products']['items'][0]); $this->assertAttributes($response['category']['products']['items'][0]); - $this->assertWebsites($firstProduct, $response['category']['products']['items'][0]['websites']); } /** @@ -541,6 +526,7 @@ public function testBreadCrumbs() name breadcrumbs { category_id + category_uid category_name category_level category_url_key @@ -556,6 +542,7 @@ public function testBreadCrumbs() 'breadcrumbs' => [ [ 'category_id' => 3, + 'category_uid' => base64_encode('3'), 'category_name' => "Category 1", 'category_level' => 2, 'category_url_key' => "category-1", @@ -563,6 +550,7 @@ public function testBreadCrumbs() ], [ 'category_id' => 4, + 'category_uid' => base64_encode('4'), 'category_name' => "Category 1.1", 'category_level' => 3, 'category_url_key' => "category-1-1", @@ -679,6 +667,7 @@ public function testBreadCrumbsWithDisabledParentCategory() name breadcrumbs { category_id + category_uid category_name } } @@ -691,6 +680,7 @@ public function testBreadCrumbsWithDisabledParentCategory() 'breadcrumbs' => [ [ 'category_id' => 3, + 'category_uid' => base64_encode('3'), 'category_name' => "Category 1", ] ] @@ -727,8 +717,6 @@ public function categoryImageDataProvider(): array private function assertBaseFields($product, $actualResponse) { $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], ['response_field' => 'name', 'expected_value' => $product->getName()], ['response_field' => 'price', 'expected_value' => [ 'minimalPrice' => [ @@ -756,30 +744,10 @@ private function assertBaseFields($product, $actualResponse) ], ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], - ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ]; $this->assertResponseFields($actualResponse, $assertionMap); } - /** - * @param ProductInterface $product - * @param array $actualResponse - */ - private function assertWebsites($product, $actualResponse) - { - $assertionMap = [ - [ - 'id' => current($product->getExtensionAttributes()->getWebsiteIds()), - 'name' => 'Main Website', - 'code' => 'base', - 'sort_order' => 0, - 'default_group_id' => '1', - 'is_default' => true, - ] - ]; - $this->assertEquals($actualResponse, $assertionMap); - } - /** * @param array $actualResponse */ @@ -794,12 +762,8 @@ private function assertAttributes($actualResponse) 'short_description', 'country_of_manufacture', 'gift_message_available', - 'new_from_date', - 'new_to_date', 'options_container', - 'special_price', - 'special_from_date', - 'special_to_date', + 'special_price' ]; foreach ($eavAttributes as $eavAttribute) { $this->assertArrayHasKey($eavAttribute, $actualResponse); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php index b19b8d519e857..ab04333de0740 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php @@ -39,7 +39,7 @@ public function testQuerySimpleProductAfterDelete() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id + id } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php index d17b434f39d9f..7c7212b9b9b26 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php @@ -11,7 +11,7 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Class ProductInMultipleStoresTest + * The GraphQl test for product in multiple stores */ class ProductInMultipleStoresTest extends GraphQlAbstract { @@ -31,8 +31,6 @@ public function testProductFromSpecificAndDefaultStore() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id - created_at id name price { @@ -45,7 +43,6 @@ public function testProductFromSpecificAndDefaultStore() } sku type_id - updated_at ... on PhysicalProductInterface { weight } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 2d0892b78c246..6c64539e38cb2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -65,6 +65,25 @@ public function testFilterForNonExistingCategory() ); } + /** + * Verify that filters id and uid can't be used at the same time + */ + public function testUidAndIdUsageErrorOnProductFilteringCategory() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('`category_id` and `category_uid` can\'t be used at the same time'); + $query = <<getId()}"} + category_uid : {eq:"{$categoryUid}"} second_test_configurable: {eq: "{$optionValue}"} }, pageSize: 3 @@ -1080,7 +1100,6 @@ public function testFilterWithinSpecificPriceRangeSortedByNameDesc() weight } type_id - attribute_set_id } total_count page_info @@ -1233,7 +1252,6 @@ public function testSearchWithFilterWithPageSizeEqualTotalCount() weight } type_id - attribute_set_id } total_count page_info @@ -1297,7 +1315,6 @@ public function testFilterByMultipleFilterFieldsSortedByMultipleSortFields() weight } type_id - attribute_set_id } total_count page_info @@ -1348,7 +1365,6 @@ public function testFilterByMultipleFilterFieldsSortedByMultipleSortFields() */ public function testFilterProductsForExactMatchingName() { - $query = <<get(CategoryRepositoryInterface::class); - $links = $productLinks->getAssignedProducts($queryCategoryId); + $links = $productLinks->getAssignedProducts( + is_numeric($queryCategoryId) ? $queryCategoryId : base64_decode($queryCategoryId) + ); $links = array_reverse($links); foreach ($response['products']['items'] as $itemIndex => $itemData) { $this->assertNotEmpty($itemData); @@ -1573,6 +1595,7 @@ public function testFilterProductsBySingleCategoryId() [ 'name' => $category->getName(), 'id' => $category->getId(), + 'uid' => base64_encode($category->getId()), 'path' => $category->getPath(), 'children_count' => $category->getChildrenCount(), 'product_count' => $category->getProductCount(), @@ -1692,7 +1715,6 @@ public function testFilterByExactSkuAndSortByPriceDesc() weight } type_id - attribute_set_id } total_count page_info @@ -2118,7 +2140,6 @@ public function testFilterWithinASpecificPriceRangeSortedByPriceDESC() { items { - attribute_set_id sku price { minimalPrice { @@ -2218,7 +2239,6 @@ public function testQueryFilterNoMatchingItems() weight } type_id - attribute_set_id } total_count page_info @@ -2275,7 +2295,6 @@ public function testQueryPageOutOfBoundException() ... on PhysicalProductInterface { weight } - attribute_set_id } total_count page_info @@ -2308,12 +2327,9 @@ public function testQueryWithNoSearchOrFilterArgumentException() { items{ id - attribute_set_id - created_at name sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -2456,7 +2472,6 @@ private function assertProductItems(array $filteredProducts, array $actualRespon $this->assertResponseFields( $productItemsInResponse[$itemIndex][0], [ - 'attribute_set_id' => $filteredProducts[$itemIndex]->getAttributeSetId(), 'sku' => $filteredProducts[$itemIndex]->getSku(), 'name' => $filteredProducts[$itemIndex]->getName(), 'price' => [ @@ -2483,7 +2498,6 @@ private function assertProductItemsWithPriceCheck(array $filteredProducts, array $this->assertResponseFields( $productItemsInResponse[$itemIndex][0], [ - 'attribute_set_id' => $filteredProducts[$itemIndex]->getAttributeSetId(), 'sku' => $filteredProducts[$itemIndex]->getSku(), 'name' => $filteredProducts[$itemIndex]->getName(), 'price' => [ @@ -2513,4 +2527,23 @@ private function assertProductItemsWithPriceCheck(array $filteredProducts, array ); } } + + /** + * Data provider for product single category filtering + * + * @return array[][] + */ + public function filterProductsBySingleCategoryIdDataProvider(): array + { + return [ + [ + 'fieldName' => 'category_id', + 'categoryId' => '333', + ], + [ + 'fieldName' => 'category_uid', + 'categoryId' => base64_encode('333'), + ], + ]; + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index 9946e74a24994..d573e2893e8f3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -45,11 +45,10 @@ public function testQueryAllFieldsSimpleProduct() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id country_of_manufacture - created_at gift_message_available id + uid categories { name url_path @@ -65,6 +64,7 @@ public function testQueryAllFieldsSimpleProduct() disabled file id + uid label media_type position @@ -86,11 +86,10 @@ public function testQueryAllFieldsSimpleProduct() } } name - new_from_date - new_to_date options_container ... on CustomizableProductInterface { options { + uid title required sort_order @@ -229,10 +228,9 @@ public function testQueryAllFieldsSimpleProduct() sku small_image{ url, label } thumbnail { url, label } - special_from_date special_price special_to_date - swatch_image + swatch_image tier_price tier_prices { @@ -243,11 +241,9 @@ public function testQueryAllFieldsSimpleProduct() website_id } type_id - updated_at url_key url_path canonical_url - websites { id name code sort_order default_group_id is_default } ... on PhysicalProductInterface { weight } @@ -276,8 +272,6 @@ public function testQueryAllFieldsSimpleProduct() $this->assertBaseFields($product, $response['products']['items'][0]); $this->assertEavAttributes($product, $response['products']['items'][0]); $this->assertOptions($product, $response['products']['items'][0]); - $this->assertArrayHasKey('websites', $response['products']['items'][0]); - $this->assertWebsites($product, $response['products']['items'][0]['websites']); self::assertEquals( 'Movable Position 2', $responseObject->getData('products/items/0/categories/0/name') @@ -303,15 +297,15 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() products(filter: {sku: {eq: "{$productSku}"}}) { items{ - attribute_set_id categories { id + uid } country_of_manufacture - created_at gift_message_available id + uid image {url, label} meta_description meta_keyword @@ -321,6 +315,7 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() disabled file id + uid label media_type position @@ -342,8 +337,6 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() } } name - new_from_date - new_to_date options_container ... on CustomizableProductInterface { field_options: options { @@ -351,6 +344,7 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() required sort_order option_id + uid ... on CustomizableFieldOption { product_sku field_option: value { @@ -462,7 +456,6 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() } sku small_image { url, label } - special_from_date special_price special_to_date swatch_image @@ -477,10 +470,8 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() website_id } type_id - updated_at url_key url_path - websites { id name code sort_order default_group_id is_default } ... on PhysicalProductInterface { weight } @@ -501,8 +492,6 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() $this->assertCount(1, $response['products']['items']); $this->assertArrayHasKey(0, $response['products']['items']); $this->assertMediaGalleryEntries($product, $response['products']['items'][0]); - $this->assertArrayHasKey('websites', $response['products']['items'][0]); - $this->assertWebsites($product, $response['products']['items'][0]['websites']); } /** @@ -548,7 +537,6 @@ public function testProductLinks() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id type_id product_links { @@ -585,9 +573,8 @@ public function testProductPrices() products(filter: {price: {from: "150.0", to: "250.0"}}) { items { - attribute_set_id - created_at id + uid name price { minimalPrice { @@ -635,7 +622,6 @@ public function testProductPrices() } sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -679,6 +665,7 @@ private function assertMediaGalleryEntries($product, $actualResponse) 'disabled' => (bool)$mediaGalleryEntry->isDisabled(), 'file' => $mediaGalleryEntry->getFile(), 'id' => $mediaGalleryEntry->getId(), + 'uid' => base64_encode($mediaGalleryEntry->getId()), 'label' => $mediaGalleryEntry->getLabel(), 'media_type' => $mediaGalleryEntry->getMediaType(), 'position' => $mediaGalleryEntry->getPosition(), @@ -744,7 +731,11 @@ private function assertOptions($product, $actualResponse) ['response_field' => 'sort_order', 'expected_value' => $option->getSortOrder()], ['response_field' => 'title', 'expected_value' => $option->getTitle()], ['response_field' => 'required', 'expected_value' => $option->getIsRequire()], - ['response_field' => 'option_id', 'expected_value' => $option->getOptionId()] + ['response_field' => 'option_id', 'expected_value' => $option->getOptionId()], + [ + 'response_field' => 'uid', + 'expected_value' => base64_encode('custom-option/' . $option->getOptionId()) + ] ]; if (!empty($option->getValues())) { @@ -813,14 +804,11 @@ private function assertOptions($product, $actualResponse) */ private function assertBaseFields($product, $actualResponse) { - $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], ['response_field' => 'id', 'expected_value' => $product->getId()], + ['response_field' => 'uid', 'expected_value' => base64_encode($product->getId())], ['response_field' => 'name', 'expected_value' => $product->getName()], - ['response_field' => 'price', 'expected_value' => - [ + ['response_field' => 'price', 'expected_value' => [ 'minimalPrice' => [ 'amount' => [ 'value' => $product->getSpecialPrice(), @@ -846,33 +834,12 @@ private function assertBaseFields($product, $actualResponse) ], ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], - ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ['response_field' => 'weight', 'expected_value' => $product->getWeight()], ]; $this->assertResponseFields($actualResponse, $assertionMap); } - /** - * @param ProductInterface $product - * @param array $actualResponse - */ - private function assertWebsites($product, $actualResponse) - { - $assertionMap = [ - [ - 'id' => current($product->getExtensionAttributes()->getWebsiteIds()), - 'name' => 'Main Website', - 'code' => 'base', - 'sort_order' => 0, - 'default_group_id' => '1', - 'is_default' => true, - ] - ]; - - $this->assertEquals($actualResponse, $assertionMap); - } - /** * @param ProductInterface $product * @param array $actualResponse @@ -905,10 +872,8 @@ private function assertEavAttributes($product, $actualResponse) 'meta_title', 'country_of_manufacture', 'gift_message_available', - 'news_from_date', 'options_container', 'special_price', - 'special_from_date', 'special_to_date', ]; $assertionMap = []; @@ -930,14 +895,6 @@ private function assertEavAttributes($product, $actualResponse) */ private function eavAttributesToGraphQlSchemaFieldTranslator(string $eavAttributeCode) { - switch ($eavAttributeCode) { - case 'news_from_date': - $eavAttributeCode = 'new_from_date'; - break; - case 'news_to_date': - $eavAttributeCode = 'new_to_date'; - break; - } return $eavAttributeCode; } @@ -956,6 +913,7 @@ public function testProductInAllAnchoredCategories() name categories { id + uid name is_anchor } @@ -982,6 +940,7 @@ public function testProductInAllAnchoredCategories() [ 'name' => $category->getName(), 'id' => $category->getId(), + 'uid' => base64_encode($category->getId()), 'is_anchor' => $category->getIsAnchor() ] ); @@ -1006,6 +965,7 @@ public function testProductWithNonAnchoredParentCategory() name categories { id + uid name is_anchor } @@ -1037,6 +997,7 @@ public function testProductWithNonAnchoredParentCategory() [ 'name' => $category->getName(), 'id' => $category->getId(), + 'uid' => base64_encode($category->getId()), 'is_anchor' => $category->getIsAnchor() ] ); @@ -1054,7 +1015,7 @@ public function testProductInNonAnchoredSubCategories() $query = << $category->getName(), 'id' => $category->getId(), + 'uid' => base64_encode($category->getId()), 'is_anchor' => $category->getIsAnchor() ] ); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php index 0982007daaa44..69c432f4cc82a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php @@ -42,6 +42,7 @@ public function testGetStoreConfig() list_per_page, catalog_default_sort_by, root_category_id + root_category_uid } } QUERY; @@ -58,6 +59,7 @@ public function testGetStoreConfig() $this->assertEquals(8, $response['storeConfig']['list_per_page']); $this->assertEquals('asc', $response['storeConfig']['catalog_default_sort_by']); $this->assertEquals(2, $response['storeConfig']['root_category_id']); + $this->assertEquals(base64_encode('2'), $response['storeConfig']['root_category_uid']); } /** @@ -88,6 +90,7 @@ public function testGetStoreConfigGlobal() list_per_page, catalog_default_sort_by, root_category_id + root_category_uid } } QUERY; @@ -104,5 +107,6 @@ public function testGetStoreConfigGlobal() $this->assertEquals(8, $response['storeConfig']['list_per_page']); $this->assertEquals('asc', $response['storeConfig']['catalog_default_sort_by']); $this->assertEquals(2, $response['storeConfig']['root_category_id']); + $this->assertEquals(base64_encode('2'), $response['storeConfig']['root_category_uid']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php index 00f0c496d8ea4..0661ca04e3b49 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php @@ -30,21 +30,16 @@ public function testQueryAllFieldsVirtualProduct() { items{ id - attribute_set_id - created_at name sku type_id - updated_at ... on PhysicalProductInterface { weight - } + } ... on VirtualProduct { - attribute_set_id name id sku - } } } @@ -84,24 +79,20 @@ public function testCannotQueryWeightOnVirtualProductException() { items{ id - attribute_set_id - created_at name sku type_id - updated_at ... on PhysicalProductInterface { weight - } + } ... on VirtualProduct { - attribute_set_id name weight id - sku + sku } } - } + } } QUERY; @@ -119,7 +110,6 @@ public function testCannotQueryWeightOnVirtualProductException() private function assertBaseFields($product, $actualResponse) { $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], ['response_field' => 'id', 'expected_value' => $product->getId()], ['response_field' => 'name', 'expected_value' => $product->getName()], ['response_field' => 'sku', 'expected_value' => $product->getSku()], diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php index c87daafa66728..e6c7851f775df 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php @@ -179,7 +179,6 @@ public function testRedirectsAndCustomInput() $actualUrls->getEntityType(), 0 ); - // querying a url that's a redirect the active redirected final url $this->queryUrlAndAssertResponse( (int) $product->getEntityId(), @@ -188,7 +187,6 @@ public function testRedirectsAndCustomInput() $actualUrls->getEntityType(), 301 ); - // create custom url that doesn't redirect /** @var UrlRewrite $urlRewriteModel */ $urlRewriteModel = $this->objectManager->create(UrlRewrite::class); @@ -209,7 +207,6 @@ public function testRedirectsAndCustomInput() $urlRewriteModel->setData($key, $value); } $urlRewriteModel->save(); - // querying a custom url that should return the target entity but relative should be the custom url $this->queryUrlAndAssertResponse( (int) $product->getEntityId(), @@ -218,7 +215,6 @@ public function testRedirectsAndCustomInput() $actualUrls->getEntityType(), 0 ); - // change custom url that does redirect $urlRewriteModel->setRedirectType('301'); $urlRewriteModel->setId($urlRewriteModel->getId()); @@ -239,10 +235,10 @@ public function testRedirectsAndCustomInput() * * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ - public function testCategoryUrlResolver() + public function testCategoryUrlResolver($categoryUrlPath = null) { $productSku = 'p002'; - $categoryUrlPath = 'cat-1.html'; + $categoryUrlPath = $categoryUrlPath ? $categoryUrlPath : 'cat-1.html'; /** @var ProductRepositoryInterface $productRepository */ $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $product = $productRepository->get($productSku, false, null, true); @@ -360,6 +356,7 @@ public function testInvalidUrlResolverInput() urlResolver(url:"{$urlPath}") { id + entity_uid relative_url type redirectCode @@ -458,6 +455,26 @@ public function testGetNonExistentUrlRewrite() $this->assertEquals(0, $response['urlResolver']['redirectCode']); } + /** + * Test for category entity with empty url suffix + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix.php + */ + public function testCategoryUrlResolverWithEmptyUrlSuffix() + { + $this->testCategoryUrlResolver('cat-1'); + } + + /** + * Tests if target_path(relative_url) is resolved for Product entity with empty url suffix + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix.php + */ + public function testProductUrlResolverWithEmptyUrlSuffix() + { + $this->testProductUrlResolver(); + } + /** * Assert response from GraphQl * @@ -480,6 +497,7 @@ private function queryUrlAndAssertResponse( urlResolver(url:"{$urlKey}") { id + entity_uid relative_url type redirectCode @@ -489,6 +507,7 @@ private function queryUrlAndAssertResponse( $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($productId, $response['urlResolver']['id']); + $this->assertEquals(base64_encode((string)$productId), $response['urlResolver']['entity_uid']); $this->assertEquals($relativePath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); $this->assertEquals($redirectCode, $response['urlResolver']['redirectCode']); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CompareList/CompareListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CompareList/CompareListTest.php new file mode 100644 index 0000000000000..645012030f439 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CompareList/CompareListTest.php @@ -0,0 +1,427 @@ +productRepository = $objectManager->get(ProductRepositoryInterface::class); + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + } + /** + * Create compare list without product + */ + public function testCreateCompareListWithoutProducts() + { + $response = $this->createCompareList(); + $uid = $response['createCompareList']['uid']; + $this->uidAssertion($uid); + } + + /** + * Create compare list with products + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testCreateCompareListWithProducts() + { + $product1 = $this->productRepository->get(self::PRODUCT_SKU_1); + $product2 = $this->productRepository->get(self::PRODUCT_SKU_2); + + $mutation = <<getId()}, {$product2->getId()}]}){ + uid + items { + product { + sku + } + } + } +} +MUTATION; + $response = $this->graphQlMutation($mutation); + $uid = $response['createCompareList']['uid']; + $this->uidAssertion($uid); + $this->itemsAssertion($response['createCompareList']['items']); + } + + /** + * Add products to compare list + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testAddProductToCompareList() + { + $compareList = $this->createCompareList(); + $uid = $compareList['createCompareList']['uid']; + $this->assertEquals(0, $compareList['createCompareList']['item_count'],'Incorrect count'); + $this->uidAssertion($uid); + $response = $this->addProductsToCompareList($uid); + $resultUid = $response['addProductsToCompareList']['uid']; + $this->uidAssertion($resultUid); + $this->itemsAssertion($response['addProductsToCompareList']['items']); + $this->assertEquals(2, $response['addProductsToCompareList']['item_count'],'Incorrect count'); + $this->assertResponseFields( + $response['addProductsToCompareList']['attributes'], + [ + [ + 'code'=> 'sku', + 'label'=> 'SKU' + ], + [ + 'code'=> 'description', + 'label'=> 'Description' + ], + [ + 'code'=> 'short_description', + 'label'=> 'Short Description' + ] + ] + ); + } + + /** + * Remove products from compare list + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testRemoveProductFromCompareList() + { + $compareList = $this->createCompareList(); + $uid = $compareList['createCompareList']['uid']; + $this->uidAssertion($uid); + $addProducts = $this->addProductsToCompareList($uid); + $this->itemsAssertion($addProducts['addProductsToCompareList']['items']); + $this->assertCount(2, $addProducts['addProductsToCompareList']['items']); + $product = $this->productRepository->get(self::PRODUCT_SKU_1); + $removeFromCompareList = <<getId()}]}) { + uid + items { + product { + sku + } + } + } +} +MUTATION; + $response = $this->graphQlMutation($removeFromCompareList); + $this->assertCount(1, $response['removeProductsFromCompareList']['items']); + } + + /** + * Get compare list query + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testGetCompareList() + { + $compareList = $this->createCompareList(); + $uid = $compareList['createCompareList']['uid']; + $this->uidAssertion($uid); + $addProducts = $this->addProductsToCompareList($uid); + $this->itemsAssertion($addProducts['addProductsToCompareList']['items']); + $query = <<graphQlQuery($query); + $this->itemsAssertion($response['compareList']['items']); + } + + /** + * Remove compare list + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testDeleteCompareList() + { + $compareList = $this->createCompareList(); + $uid = $compareList['createCompareList']['uid']; + $this->uidAssertion($uid); + $addProducts = $this->addProductsToCompareList($uid); + $this->itemsAssertion($addProducts['addProductsToCompareList']['items']); + $deleteCompareList = <<graphQlMutation($deleteCompareList); + $this->assertTrue($response['deleteCompareList']['result']); + $response1 = $this->graphQlMutation($deleteCompareList); + $this->assertFalse($response1['deleteCompareList']['result']); + } + + /** + * Assign compare list to customer + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testAssignCompareListToCustomer() + { + $compareList = $this->createCompareList(); + $uid = $compareList['createCompareList']['uid']; + $this->uidAssertion($uid); + $addProducts = $this->addProductsToCompareList($uid); + $this->itemsAssertion($addProducts['addProductsToCompareList']['items']); + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $customerQuery = <<graphQlQuery( + $customerQuery, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + $this->assertArrayHasKey('compare_list', $customerResponse['customer']); + $this->assertNull($customerResponse['customer']['compare_list']); + + $assignCompareListToCustomer = <<graphQlMutation( + $assignCompareListToCustomer, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + $this->assertTrue($assignResponse['assignCompareListToCustomer']['result']); + + $customerAssignedResponse = $this->graphQlQuery( + $customerQuery, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('compare_list', $customerAssignedResponse['customer']); + $this->uidAssertion($customerAssignedResponse['customer']['compare_list']['uid']); + $this->itemsAssertion($customerAssignedResponse['customer']['compare_list']['items']); + } + + /** + * Assign compare list of one customer to another customer + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + */ + public function testCompareListsNotAccessibleBetweenCustomers() + { + $uidCustomer1 = $this->createCompareListForCustomer('customer@example.com', 'password'); + $uidcustomer2 = $this->createCompareListForCustomer('customer_two@example.com', 'password'); + $assignCompareListToCustomer = <<expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage($expectedExceptionsMessage); + //customer2 not allowed to assign compareList belonging to customer1 + $this->graphQlMutation( + $assignCompareListToCustomer, + [], + '', + $this->getCustomerAuthHeaders('customer_two@example.com', 'password') + ); + + $deleteCompareList = <<expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage($expectedExceptionsMessage); + //customer1 not allowed to delete compareList belonging to customer2 + $this->graphQlMutation( + $assignCompareListToCustomer, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + + } + + /** + * Get customer Header + * + * @param string $email + * @param string $password + * + * @return array + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Create compare list + * + * @return array + */ + private function createCompareList(): array + { + $mutation = <<graphQlMutation($mutation); + } + + private function createCompareListForCustomer(string $username, string $password): string + { + $compareListCustomer = <<graphQlMutation( + $compareListCustomer, + [], + '', + $this->getCustomerAuthHeaders($username, $password) + ); + + return $response['createCompareList']['uid']; + } + + /** + * Add products to compare list + * + * @param $uid + * + * @return array + */ + private function addProductsToCompareList($uid): array + { + $product1 = $this->productRepository->get(self::PRODUCT_SKU_1); + $product2 = $this->productRepository->get(self::PRODUCT_SKU_2); + $addProductsToCompareList = <<getId()}, {$product2->getId()}]}) { + uid + item_count + attributes{code label} + items { + product { + sku + } + } + } +} +MUTATION; + return $this->graphQlMutation($addProductsToCompareList); + } + + /** + * Assert UID + * + * @param string $uid + */ + private function uidAssertion(string $uid) + { + $this->assertIsString($uid); + $this->assertEquals(32, strlen($uid)); + } + + /** + * Assert products + * + * @param array $items + */ + private function itemsAssertion(array $items) + { + $this->assertArrayHasKey(0, $items); + $this->assertArrayHasKey(1, $items); + $this->assertEquals(self::PRODUCT_SKU_1, $items[0]['product']['sku']); + $this->assertEquals(self::PRODUCT_SKU_2, $items[1]['product']['sku']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php index fb6b36b883e77..5a08692d5dcdd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php @@ -64,8 +64,9 @@ public function testAddConfigurableProductToCart() $parentSku = $product['sku']; $attributeId = (int) $product['configurable_options'][0]['attribute_id']; $valueIndex = $product['configurable_options'][0]['values'][1]['value_index']; - + $productRowId = (string) $product['configurable_options'][0]['product_id']; $selectedConfigurableOptionsQuery = $this->generateSuperAttributesUIDQuery($attributeId, $valueIndex); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); $query = $this->getQuery( @@ -76,19 +77,35 @@ public function testAddConfigurableProductToCart() ); $response = $this->graphQlMutation($query); - + $expectedProductOptionsValueUid = $this->generateConfigurableSelectionUID($attributeId, $valueIndex); + $expectedProductOptionsUid = base64_encode("configurable/$productRowId/$attributeId"); $cartItem = current($response['addProductsToCart']['cart']['items']); self::assertEquals($quantity, $cartItem['quantity']); self::assertEquals($parentSku, $cartItem['product']['sku']); + self::assertEquals(base64_encode((string)$cartItem['product']['id']), $cartItem['product']['uid']); self::assertArrayHasKey('configurable_options', $cartItem); $option = current($cartItem['configurable_options']); self::assertEquals($attributeId, $option['id']); self::assertEquals($valueIndex, $option['value_id']); + self::assertEquals($expectedProductOptionsValueUid, $option['configurable_product_option_value_uid']); + self::assertEquals($expectedProductOptionsUid, $option['configurable_product_option_uid']); self::assertArrayHasKey('option_label', $option); self::assertArrayHasKey('value_label', $option); } + /** + * Generates UID configurable product + * + * @param int $attributeId + * @param int $valueIndex + * @return string + */ + private function generateConfigurableSelectionUID(int $attributeId, int $valueIndex): string + { + return base64_encode("configurable/$attributeId/$valueIndex"); + } + /** * Generates UID for super configurable product super attributes * @@ -98,7 +115,7 @@ public function testAddConfigurableProductToCart() */ private function generateSuperAttributesUIDQuery(int $attributeId, int $valueIndex): string { - return 'selected_options: ["' . base64_encode("configurable/$attributeId/$valueIndex") . '"]'; + return 'selected_options: ["' . $this->generateConfigurableSelectionUID($attributeId, $valueIndex) . '"]'; } /** @@ -256,15 +273,20 @@ private function getQuery( cart { items { id + uid quantity product { sku + uid + id } ... on ConfigurableCartItem { configurable_options { id + configurable_product_option_uid option_label value_id + configurable_product_option_value_uid value_label } } @@ -306,16 +328,20 @@ private function getFetchProductQuery(string $term): string ) { items { sku + uid ... on ConfigurableProduct { configurable_options { attribute_id + attribute_uid attribute_code id + uid label position product_id use_default values { + uid default_label label store_label @@ -323,6 +349,17 @@ private function getFetchProductQuery(string $term): string value_index } } + configurable_options_selection_metadata { + options_available_for_selection { + attribute_code + option_value_uids + } + variant { + uid + name + attribute_set_id + } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php index 49bbabc212fc2..a5431efefdde3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php @@ -39,12 +39,9 @@ public function testQueryConfigurableProductLinks() products(filter: {sku: {eq: "{$productSku}"}}) { items { id - attribute_set_id - created_at name sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -115,12 +112,9 @@ public function testQueryConfigurableProductLinks() id name sku - attribute_set_id ... on PhysicalProductInterface { weight } - created_at - updated_at price { minimalPrice { amount { @@ -237,8 +231,6 @@ private function assertBaseFields($product, $actualResponse) /** @var MetadataPool $metadataPool */ $metadataPool = ObjectManager::getInstance()->get(MetadataPool::class); $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], [ 'response_field' => 'id', 'expected_value' => $product->getData( @@ -250,7 +242,6 @@ private function assertBaseFields($product, $actualResponse) ['response_field' => 'name', 'expected_value' => $product->getName()], ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], - ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ['response_field' => 'weight', 'expected_value' => $product->getWeight()], [ 'response_field' => 'price', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/RemoveConfigurableProductFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/RemoveConfigurableProductFromCartTest.php index f1b08d8858ba0..8be20653070e1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/RemoveConfigurableProductFromCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/RemoveConfigurableProductFromCartTest.php @@ -53,14 +53,20 @@ protected function setUp(): void } /** + * @param string $itemArgName + * @param string $reservedOrderId + * @dataProvider removeConfigurableProductFromCartDataProvider * @magentoApiDataFixture Magento/ConfigurableProduct/_files/quote_with_configurable_product.php */ - public function testRemoveConfigurableProductFromCart() + public function testRemoveConfigurableProductFromCart(string $itemArgName, string $reservedOrderId) { $configurableOptionSku = 'simple_10'; - $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_cart_with_configurable'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); $quoteItemId = $this->getQuoteItemIdBySku($configurableOptionSku); - $query = $this->getQuery($maskedQuoteId, $quoteItemId); + if ($itemArgName === 'cart_item_uid') { + $quoteItemId = base64_encode($quoteItemId); + } + $query = $this->getQuery($itemArgName, $maskedQuoteId, $quoteItemId); $response = $this->graphQlMutation($query); $this->assertArrayHasKey('cart', $response['removeItemFromCart']); @@ -69,18 +75,37 @@ public function testRemoveConfigurableProductFromCart() } /** + * Data provider for testUpdateConfigurableCartItemQuantity + * + * @return array + */ + public function removeConfigurableProductFromCartDataProvider(): array + { + return [ + ['cart_item_id', 'test_cart_with_configurable'], + ['cart_item_uid', 'test_cart_with_configurable'], + ]; + } + + /** + * @param string $itemArgName * @param string $maskedQuoteId - * @param int $itemId + * @param string $itemId * @return string */ - private function getQuery(string $maskedQuoteId, int $itemId): string + private function getQuery(string $itemArgName, string $maskedQuoteId, string $itemId): string { + if (is_numeric($itemId)) { + $itemId = (int) $itemId; + } else { + $itemId = '"' . $itemId . '"'; + } return <<quoteFactory->create(); $this->quoteResource->load($quote, 'test_cart_with_configurable', 'reserved_order_id'); @@ -107,7 +132,7 @@ private function getQuoteItemIdBySku(string $sku): int $quoteItemsCollection = $quote->getItemsCollection(); foreach ($quoteItemsCollection->getItems() as $item) { if ($item->getSku() == $sku) { - return (int)$item->getId(); + return $item->getId(); } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/StoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/StoreConfigTest.php new file mode 100644 index 0000000000000..0c4dca9a642d2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/StoreConfigTest.php @@ -0,0 +1,38 @@ +graphQlQuery($query); + self::assertArrayHasKey('configurable_thumbnail_source', $response['storeConfig']); + self::assertEquals('itself', $response['storeConfig']['configurable_thumbnail_source']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php index d3bc0204efe23..c7800ec327e90 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php @@ -44,18 +44,22 @@ class UpdateConfigurableCartItemsTest extends GraphQlAbstract private $quoteResource; /** + * @param string $itemArgName + * @param string $reservedOrderId + * @dataProvider updateConfigurableCartItemQuantityDataProvider * @magentoApiDataFixture Magento/ConfigurableProduct/_files/quote_with_configurable_product.php */ - public function testUpdateConfigurableCartItemQuantity() + public function testUpdateConfigurableCartItemQuantity(string $itemArgName, string $reservedOrderId) { - $reservedOrderId = 'test_cart_with_configurable'; $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); $productSku = 'simple_10'; $newQuantity = 123; - $quoteItem = $this->getQuoteItemBySku($productSku, $reservedOrderId); - - $query = $this->getQuery($maskedQuoteId, (int)$quoteItem->getId(), $newQuantity); + $quoteItemId = $this->getQuoteItemBySku($productSku, $reservedOrderId)->getId(); + if ($itemArgName === 'cart_item_uid') { + $quoteItemId = base64_encode($quoteItemId); + } + $query = $this->getQuery($itemArgName, $maskedQuoteId, $quoteItemId, $newQuantity); $response = $this->graphQlMutation($query); self::assertArrayHasKey('updateCartItems', $response); @@ -63,6 +67,19 @@ public function testUpdateConfigurableCartItemQuantity() self::assertEquals($newQuantity, $response['updateCartItems']['cart']['items']['0']['quantity']); } + /** + * Data provider for testUpdateConfigurableCartItemQuantity + * + * @return array + */ + public function updateConfigurableCartItemQuantityDataProvider(): array + { + return [ + ['cart_item_id', 'test_cart_with_configurable'], + ['cart_item_uid', 'test_cart_with_configurable'], + ]; + } + /** * @inheritdoc */ @@ -76,20 +93,26 @@ protected function setUp(): void } /** + * @param string $itemArgName * @param string $maskedQuoteId - * @param int $quoteItemId + * @param string $quoteItemId * @param int $newQuantity * @return string */ - private function getQuery(string $maskedQuoteId, int $quoteItemId, int $newQuantity): string + private function getQuery(string $itemArgName, string $maskedQuoteId, string $quoteItemId, int $newQuantity): string { + if (is_numeric($quoteItemId)) { + $quoteItemId = (int) $quoteItemId; + } else { + $quoteItemId = '"' . $quoteItemId . '"'; + } return <<expectException(ResponseContainsErrorsException::class); $this->expectExceptionMessage($expectedExceptionsMessage); @@ -44,31 +45,30 @@ public function testSimpleInputArgumentRequired() /** * Test that a more complex required argument is handled properly * - * updateCartItems mutation has required parameter input.cart_items.cart_item_id + * testInputQueryWithMandatoryArguments mutation has required parameter input.query_items.query_item_id */ public function testInputObjectArgumentRequired() { $query = <<expectException(ResponseContainsErrorsException::class); $this->expectExceptionMessage($expectedExceptionsMessage); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/AddGroupedProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/AddGroupedProductToWishlistTest.php index a9c9e0e104235..7a5fdb0a242fa 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/AddGroupedProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/AddGroupedProductToWishlistTest.php @@ -59,9 +59,9 @@ public function testAllFieldsGroupedProduct() $this->assertEquals($wishlist->getItemsCount(), $response['items_count']); $this->assertEquals($wishlist->getSharingCode(), $response['sharing_code']); $this->assertEquals($wishlist->getUpdatedAt(), $response['updated_at']); - $this->assertEquals((int) $item->getQty(), $response['items_v2'][0]['quantity']); - $this->assertEquals($item->getAddedAt(), $response['items_v2'][0]['added_at']); - $this->assertEquals($productSku, $response['items_v2'][0]['product']['sku']); + $this->assertEquals((int) $item->getQty(), $response['items_v2']['items'][0]['quantity']); + $this->assertEquals($item->getAddedAt(), $response['items_v2']['items'][0]['added_at']); + $this->assertEquals($productSku, $response['items_v2']['items'][0]['product']['sku']); } private function getMutation( @@ -90,13 +90,16 @@ private function getMutation( items_count updated_at items_v2 { - id + items{ + id description quantity added_at product { sku } + } + } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php index 8cb0a6db972b4..ac893aa10fd55 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php @@ -42,8 +42,6 @@ public function testAllFieldsGroupedProduct() products(filter: {sku: {eq: "{$productSku}"}}) { items { id - attribute_set_id - created_at name sku type_id diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/ProductViewTest.php index 9eea2396c24ce..86ba62f8e41ec 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/ProductViewTest.php @@ -106,13 +106,11 @@ private function getQuery(string $sku): string return <<registry = Bootstrap::getObjectManager()->get(Registry::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + } + + /** + * Test setting allow_remote_shopping_assistance to true + * + * @throws \Exception + */ + public function testCreateCustomerAccountWithAllowTrue() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<graphQlMutation($query); + + $this->assertNull($response['createCustomerV2']['customer']['id']); + $this->assertEquals($newFirstname, $response['createCustomerV2']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomerV2']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomerV2']['customer']['email']); + $this->assertTrue($response['createCustomerV2']['customer']['is_subscribed']); + $this->assertTrue($response['createCustomerV2']['customer']['allow_remote_shopping_assistance']); + } + + /** + * Test setting allow_remote_shopping_assistance to false + * + * @throws \Exception + */ + public function testCreateCustomerAccountWithAllowFalse() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<graphQlMutation($query); + + $this->assertNull($response['createCustomerV2']['customer']['id']); + $this->assertEquals($newFirstname, $response['createCustomerV2']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomerV2']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomerV2']['customer']['email']); + $this->assertTrue($response['createCustomerV2']['customer']['is_subscribed']); + $this->assertFalse($response['createCustomerV2']['customer']['allow_remote_shopping_assistance']); + } + + /** + * Test omitting allow_remote_shopping_assistance + * + * @throws \Exception + */ + public function testCreateCustomerAccountWithoutAllow() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<graphQlMutation($query); + + $this->assertNull($response['createCustomerV2']['customer']['id']); + $this->assertEquals($newFirstname, $response['createCustomerV2']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomerV2']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomerV2']['customer']['email']); + $this->assertTrue($response['createCustomerV2']['customer']['is_subscribed']); + $this->assertFalse($response['createCustomerV2']['customer']['allow_remote_shopping_assistance']); + } + + protected function tearDown(): void + { + $newEmail = 'new_customer@example.com'; + try { + $customer = $this->customerRepository->get($newEmail); + } catch (\Exception $exception) { + return; + } + + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $this->customerRepository->delete($customer); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/GenerateLoginCustomerTokenTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/GenerateLoginCustomerTokenTest.php new file mode 100755 index 0000000000000..6d7731836656e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/GenerateLoginCustomerTokenTest.php @@ -0,0 +1,227 @@ +customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->adminTokenService = $objectManager->get(AdminTokenService::class); + } + + /** + * Verify with Admin email ID and Magento_LoginAsCustomer::login is enabled + * + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/admin.php + * @magentoConfigFixture admin_store login_as_customer/general/enabled 1 + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/customer.php + * @throws Exception + */ + public function testGenerateCustomerValidTokenLoginAsCustomerEnabled() + { + $customerEmail = 'customer@example.com'; + + $mutation = $this->getQuery($customerEmail); + + $response = $this->graphQlMutation( + $mutation, + [], + '', + $this->getAdminHeaderAuthentication('TestAdmin1', \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD) + ); + $this->assertArrayHasKey('generateCustomerTokenAsAdmin', $response); + $this->assertIsArray($response['generateCustomerTokenAsAdmin']); + } + + /** + * Verify with Admin email ID and Magento_LoginAsCustomer::login is disabled + * + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/admin.php + * @magentoConfigFixture admin_store login_as_customer/general/enabled 0 + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/customer.php + * @throws Exception + */ + public function testGenerateCustomerValidTokenLoginAsCustomerDisabled() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("Login as Customer is disabled."); + + $customerEmail = 'customer@example.com'; + + $mutation = $this->getQuery($customerEmail); + $response = $this->graphQlMutation( + $mutation, + [], + '', + $this->getAdminHeaderAuthentication('TestAdmin1', \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD) + ); + } + + /** + * Verify with Customer Token in auth header + * + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/customer.php + * @magentoConfigFixture admin_store login_as_customer/general/enabled 1 + * @throws Exception + */ + public function testGenerateCustomerTokenLoginWithCustomerCredentials() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("The current customer isn't authorized."); + + $customerEmail = 'customer@example.com'; + $password = 'password'; + + $mutation = $this->getQuery($customerEmail); + + $this->graphQlMutation( + $mutation, + [], + '', + $this->getCustomerHeaderAuthentication($customerEmail, $password) + ); + } + + /** + * Test with invalid data. + * + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/admin.php + * @magentoConfigFixture admin_store login_as_customer/general/enabled 1 + * + * @dataProvider dataProviderInvalidInfo + * @param string $adminUserName + * @param string $adminPassword + * @param string $customerEmail + * @param string $message + */ + public function testGenerateCustomerTokenInvalidData( + string $adminUserName, + string $adminPassword, + string $customerEmail, + string $message + ) { + $this->expectException(Exception::class); + $this->expectExceptionMessage($message); + + $mutation = $this->getQuery($customerEmail); + $this->graphQlMutation( + $mutation, + [], + '', + $this->getAdminHeaderAuthentication($adminUserName, $adminPassword) + ); + } + + /** + * Provides invalid test cases data + * + * @return array + */ + public function dataProviderInvalidInfo(): array + { + return [ + 'invalid_admin_user_name' => [ + 'TestAdmin(^%', + \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD, + 'customer@example.com', + 'The account sign-in was incorrect or your account is disabled temporarily. ' . + 'Please wait and try again later.' + ], + 'invalid_admin_password' => [ + 'TestAdmin1', + 'invalid_password', + 'customer@example.com', + 'The account sign-in was incorrect or your account is disabled temporarily. ' . + 'Please wait and try again later.' + ] + ]; + } + + /** + * @param string $customerEmail + * @return string + */ + private function getQuery(string $customerEmail) : string + { + return <<customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * To get admin access token + * + * @param string $userName + * @param string $password + * @return string[] + * @throws AuthenticationException + */ + private function getAdminHeaderAuthentication(string $userName, string $password) + { + try { + $adminAccessToken = $this->adminTokenService->createAdminAccessToken($userName, $password); + return ['Authorization' => 'Bearer ' . $adminAccessToken]; + } catch (\Exception $e) { + throw new AuthenticationException( + __( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + ) + ); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/UpdateCustomerV2Test.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/UpdateCustomerV2Test.php new file mode 100644 index 0000000000000..f47f7c1d1c3db --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/UpdateCustomerV2Test.php @@ -0,0 +1,76 @@ +customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomer(): void + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertTrue($response['updateCustomerV2']['customer']['allow_remote_shopping_assistance']); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } + +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php index 20a612e9f88b0..40280f5c7b2c7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php @@ -93,8 +93,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrencyNonExisti products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id - created_at id name price { @@ -107,7 +105,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrencyNonExisti } sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -138,8 +135,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrencyNotAllowe products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id - created_at id name price { @@ -152,7 +147,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrencyNotAllowe } sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -187,8 +181,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrency() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id - created_at id name price { @@ -201,7 +193,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrency() } sku type_id - updated_at ... on PhysicalProductInterface { weight } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php index 4e50f6ff3a2ca..418258478d4d7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php @@ -81,10 +81,31 @@ public function testAddSimpleProductWithOptions() $customizableOptionsOutput = $response['addProductsToCart']['cart']['items'][0]['customizable_options']; - foreach ($customizableOptionsOutput as $customizableOptionOutput) { + foreach ($customizableOptionsOutput as $key => $customizableOptionOutput) { $customizableOptionOutputValues = []; foreach ($customizableOptionOutput['values'] as $customizableOptionOutputValue) { $customizableOptionOutputValues[] = $customizableOptionOutputValue['value']; + + $decodedOptionValue = base64_decode($customizableOptionOutputValue['customizable_option_value_uid']); + $decodedArray = explode('/', $decodedOptionValue); + if (count($decodedArray) === 2) { + self::assertEquals( + base64_encode('custom-option/' . $customizableOptionOutput['id']), + $customizableOptionOutputValue['customizable_option_value_uid'] + ); + } elseif (count($decodedArray) === 3) { + self::assertEquals( + base64_encode( + 'custom-option/' + . $customizableOptionOutput['id'] + . '/' + . $customizableOptionOutputValue['value'] + ), + $customizableOptionOutputValue['customizable_option_value_uid'] + ); + } else { + self::fail('customizable_option_value_uid '); + } } if (count($customizableOptionOutputValues) === 1) { $customizableOptionOutputValues = $customizableOptionOutputValues[0]; @@ -94,6 +115,11 @@ public function testAddSimpleProductWithOptions() $decodedItemOptions[$customizableOptionOutput['id']], $customizableOptionOutputValues ); + + self::assertEquals( + base64_encode((string) 'custom-option/' . $customizableOptionOutput['id']), + $customizableOptionOutput['customizable_option_uid'] + ); } } @@ -242,8 +268,11 @@ private function getAddToCartMutation( customizable_options { label id + customizable_option_uid values { value + customizable_option_value_uid + id } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php index d51e632035dfc..f31b396a18ba6 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php @@ -188,13 +188,14 @@ public function testOptionSetPersistsOnExtraOptionWithIncorrectId() */ private function getQuery(string $maskedQuoteId, int $quoteItemId, $customizableOptionsQuery): string { + $base64EncodedItemId = base64_encode((string) $quoteItemId); return <<getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $headerMap = ['Store' => 'fixture_second_store']; + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $response = $this->graphQlMutation($query, [], '', $headerMap); + self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); + + self::assertArrayHasKey('shipping_addresses', $response['addSimpleProductsToCart']['cart']); + self::assertEmpty($response['addSimpleProductsToCart']['cart']['shipping_addresses']); + self::assertEquals($quantity, $response['addSimpleProductsToCart']['cart']['items'][0]['quantity']); + self::assertEquals($sku, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + self::assertArrayHasKey('prices', $response['addSimpleProductsToCart']['cart']['items'][0]); + self::assertArrayHasKey('id', $response['addSimpleProductsToCart']['cart']); + self::assertEquals($maskedQuoteId, $response['addSimpleProductsToCart']['cart']['id']); + + self::assertArrayHasKey('price', $response['addSimpleProductsToCart']['cart']['items'][0]['prices']); + $price = $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['price']; + self::assertArrayHasKey('value', $price); + self::assertEquals(10, $price['value']); + self::assertArrayHasKey('currency', $price); + self::assertEquals('USD', $price['currency']); + + self::assertArrayHasKey('row_total', $response['addSimpleProductsToCart']['cart']['items'][0]['prices']); + $rowTotal = $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['row_total']; + self::assertArrayHasKey('value', $rowTotal); + self::assertEquals(20, $rowTotal['value']); + self::assertArrayHasKey('currency', $rowTotal); + self::assertEquals('USD', $rowTotal['currency']); + + self::assertArrayHasKey( + 'row_total_including_tax', + $response['addSimpleProductsToCart']['cart']['items'][0]['prices'] + ); + $rowTotalIncludingTax = + $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['row_total_including_tax']; + self::assertArrayHasKey('value', $rowTotalIncludingTax); + self::assertEquals(20, $rowTotalIncludingTax['value']); + self::assertArrayHasKey('currency', $rowTotalIncludingTax); + self::assertEquals('USD', $rowTotalIncludingTax['currency']); + } + /** * @magentoApiDataFixture Magento/Catalog/_files/product_with_image_no_options.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php index 858c38cc72dfd..b2c6902f5aa97 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php @@ -8,7 +8,12 @@ namespace Magento\GraphQl\Quote\Guest; use Exception; +use Magento\Config\App\Config\Type\System; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Directory\Model\Currency; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -91,7 +96,9 @@ public function testGetCartIfCartIdIsEmpty() public function testGetCartIfCartIdIsMissed() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Field "cart" argument "cart_id" of type "String!" is required but not provided.'); + $this->expectExceptionMessage( + 'Field "cart" argument "cart_id" of type "String!" is required but not provided.' + ); $query = <<getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = ['Store' => 'fixture_second_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Store/_files/second_store_with_second_currency.php + */ + public function testGetCartWithDifferentStoreDifferentCurrency() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute( + 'test_order_with_simple_product_without_address' + ); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = ['Store' => 'fixture_second_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + self::assertArrayHasKey('prices', $response['cart']['items'][0]); + $price = $response['cart']['items'][0]['prices']['price']; + self::assertEquals(20, $price['value']); + self::assertEquals('EUR', $price['currency']); + + // test alternate currency in header + $objectManager = Bootstrap::getObjectManager(); + $store = $objectManager->create(Store::class); + $store->load('fixture_second_store', 'code'); + if ($storeId = $store->load('fixture_second_store', 'code')->getId()) { + /** @var \Magento\Config\Model\ResourceModel\Config $configResource */ + $configResource = $objectManager->get(Config::class); + $configResource->saveConfig( + Currency::XML_PATH_CURRENCY_ALLOW, + 'USD', + ScopeInterface::SCOPE_STORES, + $storeId + ); + /** + * Configuration cache clean is required to reload currency setting + */ + /** @var System $config */ + $config = $objectManager->get(System::class); + $config->clean(); + } + $headerMap['Content-Currency'] = 'USD'; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + self::assertArrayHasKey('prices', $response['cart']['items'][0]); + $price = $response['cart']['items'][0]['prices']['price']; + self::assertEquals(10, $price['value']); + self::assertEquals('USD', $price['currency']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + */ + public function testGetCartWithDifferentStoreDifferentWebsite() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Wrong store code specified for cart'); + $this->expectExceptionMessage('Can\'t assign cart to store in different website.'); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); $query = $this->getQuery($maskedQuoteId); @@ -199,6 +273,12 @@ private function getQuery(string $maskedQuoteId): string product { sku } + prices { + price { + value + currency + } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php index cb210b180682c..f2cf90c95de18 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php @@ -25,13 +25,12 @@ public function testQueryRelatedProducts() { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { related_products { sku name url_key - created_at } } } @@ -60,13 +59,12 @@ public function testQueryDisableRelatedProduct() { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { related_products { sku name url_key - created_at } } } @@ -94,13 +92,12 @@ public function testQueryCrossSellProducts() { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { crosssell_products { sku name url_key - created_at } } } @@ -119,11 +116,9 @@ public function testQueryCrossSellProducts() self::assertArrayHasKey('sku', $crossSellProduct); self::assertArrayHasKey('name', $crossSellProduct); self::assertArrayHasKey('url_key', $crossSellProduct); - self::assertArrayHasKey('created_at', $crossSellProduct); self::assertEquals($crossSellProduct['sku'], 'simple'); self::assertEquals($crossSellProduct['name'], 'Simple Cross Sell'); self::assertEquals($crossSellProduct['url_key'], 'simple-cross-sell'); - self::assertNotEmpty($crossSellProduct['created_at']); } /** @@ -137,13 +132,12 @@ public function testQueryUpSellProducts() { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { upsell_products { sku name url_key - created_at } } } @@ -162,11 +156,9 @@ public function testQueryUpSellProducts() self::assertArrayHasKey('sku', $upSellProduct); self::assertArrayHasKey('name', $upSellProduct); self::assertArrayHasKey('url_key', $upSellProduct); - self::assertArrayHasKey('created_at', $upSellProduct); self::assertEquals($upSellProduct['sku'], 'simple'); self::assertEquals($upSellProduct['name'], 'Simple Up Sell'); self::assertEquals($upSellProduct['url_key'], 'simple-up-sell'); - self::assertNotEmpty($upSellProduct['created_at']); } /** @@ -190,14 +182,12 @@ private function assertRelatedProducts(array $relatedProducts): void self::assertArrayHasKey('sku', $product); self::assertArrayHasKey('name', $product); self::assertArrayHasKey('url_key', $product); - self::assertArrayHasKey('created_at', $product); self::assertArrayHasKey($product['sku'], $expectedData); $productExpectedData = $expectedData[$product['sku']]; self::assertEquals($product['name'], $productExpectedData['name']); self::assertEquals($product['url_key'], $productExpectedData['url_key']); - self::assertNotEmpty($product['created_at']); } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php index b2d25c7418866..e2897d6e16bfe 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php @@ -144,8 +144,6 @@ public function testQueryAllFieldsSimpleProduct() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id - created_at id name price { @@ -194,7 +192,6 @@ public function testQueryAllFieldsSimpleProduct() } sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -281,8 +278,6 @@ private function assertBaseFields($product, $actualResponse) } // product_object_field_name, expected_value $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], ['response_field' => 'id', 'expected_value' => $product->getId()], ['response_field' => 'name', 'expected_value' => $product->getName()], ['response_field' => 'price', 'expected_value' => @@ -345,7 +340,6 @@ private function assertBaseFields($product, $actualResponse) ], ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], - ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ['response_field' => 'weight', 'expected_value' => $product->getWeight()], ]; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php index ab3fed044ea97..594863f319b2b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php @@ -21,8 +21,8 @@ public function testMutation() $query = <<assertArrayHasKey('addProductsToWishlist', $response); $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $this->assertEmpty($response['addProductsToWishlist']['user_errors']); $response = $response['addProductsToWishlist']['wishlist']; $this->assertEquals($wishlist->getItemsCount(), $response['items_count']); $this->assertEquals($wishlist->getSharingCode(), $response['sharing_code']); $this->assertEquals($wishlist->getUpdatedAt(), $response['updated_at']); - $this->assertEquals($item->getData('qty'), $response['items_v2'][0]['quantity']); - $this->assertEquals($item->getDescription(), $response['items_v2'][0]['description']); - $this->assertEquals($item->getAddedAt(), $response['items_v2'][0]['added_at']); - $this->assertNotEmpty($response['items_v2'][0]['bundle_options']); - $bundleOptions = $response['items_v2'][0]['bundle_options']; + $this->assertEquals($item->getData('qty'), $response['items_v2']['items'][0]['quantity']); + $this->assertEquals($item->getDescription(), $response['items_v2']['items'][0]['description']); + $this->assertEquals($item->getAddedAt(), $response['items_v2']['items'][0]['added_at']); + $this->assertNotEmpty($response['items_v2']['items'][0]['bundle_options']); + $bundleOptions = $response['items_v2']['items'][0]['bundle_options']; $this->assertEquals('Bundle Product Items', $bundleOptions[0]['label']); $this->assertEquals(Select::NAME, $bundleOptions[0]['type']); + $bundleOptionValuesResponse = $bundleOptions[0]['values'][0]; + $this->assertNotNull($bundleOptionValuesResponse['id']); + unset($bundleOptionValuesResponse['id']); + $this->assertResponseFields( + $bundleOptionValuesResponse, + [ + 'label' => 'Simple Product', + 'quantity' => 1, + 'price' => 2.75 + ] + ); } /** @@ -121,21 +133,22 @@ public function testAddingBundleItemWithCustomOptionQuantity() $this->assertArrayHasKey('addProductsToWishlist', $response); $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $this->assertEmpty($response['addProductsToWishlist']['user_errors']); $response = $response['addProductsToWishlist']['wishlist']; $this->assertEquals($wishlist->getItemsCount(), $response['items_count']); $this->assertEquals($wishlist->getSharingCode(), $response['sharing_code']); $this->assertEquals($wishlist->getUpdatedAt(), $response['updated_at']); - $this->assertEquals($item->getData('qty'), $response['items_v2'][0]['quantity']); - $this->assertEquals($item->getDescription(), $response['items_v2'][0]['description']); - $this->assertEquals($item->getAddedAt(), $response['items_v2'][0]['added_at']); - $this->assertNotEmpty($response['items_v2'][0]['bundle_options']); - $bundleOptions = $response['items_v2'][0]['bundle_options']; + $this->assertEquals($item->getData('qty'), $response['items_v2']['items'][0]['quantity']); + $this->assertEquals($item->getDescription(), $response['items_v2']['items'][0]['description']); + $this->assertEquals($item->getAddedAt(), $response['items_v2']['items'][0]['added_at']); + $this->assertNotEmpty($response['items_v2']['items'][0]['bundle_options']); + $bundleOptions = $response['items_v2']['items'][0]['bundle_options']; $this->assertEquals('Option 1', $bundleOptions[0]['label']); - $bundleOptionOneValues = $bundleOptions[0]['values']; - $this->assertEquals(7, $bundleOptionOneValues[0]['quantity']); + $bundleOptionFirstValue = $bundleOptions[0]['values']; + $this->assertEquals(7, $bundleOptionFirstValue[0]['quantity']); $this->assertEquals('Option 2', $bundleOptions[1]['label']); - $bundleOptionTwoValues = $bundleOptions[1]['values']; - $this->assertEquals(1, $bundleOptionTwoValues[0]['quantity']); + $bundleOptionSecondValue = $bundleOptions[1]['values']; + $this->assertEquals(1, $bundleOptionSecondValue[0]['quantity']); } /** @@ -195,7 +208,8 @@ private function getQuery( items_count updated_at items_v2 { - id + items { + id description quantity added_at @@ -212,6 +226,8 @@ private function getQuery( } } } + } + } } } @@ -267,7 +283,8 @@ private function getQueryWithCustomOptionQuantity( items_count updated_at items_v2 { - id + items { + id description quantity added_at @@ -284,6 +301,8 @@ private function getQueryWithCustomOptionQuantity( } } } + } + } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php index cffc5eb6f93c1..25933e341564e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php @@ -68,17 +68,19 @@ public function testAddConfigurableProductWithOptions(): void $this->assertArrayHasKey('addProductsToWishlist', $response); $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $this->assertEmpty($response['addProductsToWishlist']['user_errors']); $wishlistResponse = $response['addProductsToWishlist']['wishlist']; $this->assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); $this->assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); $this->assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); - $this->assertEquals($wishlistItem->getId(), $wishlistResponse['items_v2'][0]['id']); - $this->assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items_v2'][0]['quantity']); - $this->assertEquals($wishlistItem->getDescription(), $wishlistResponse['items_v2'][0]['description']); - $this->assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items_v2'][0]['added_at']); - $this->assertNotEmpty($wishlistResponse['items_v2'][0]['configurable_options']); - $configurableOptions = $wishlistResponse['items_v2'][0]['configurable_options']; + $this->assertEquals($wishlistItem->getId(), $wishlistResponse['items_v2']['items'][0]['id']); + $this->assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items_v2']['items'][0]['quantity']); + $this->assertEquals($wishlistItem->getDescription(), $wishlistResponse['items_v2']['items'][0]['description']); + $this->assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items_v2']['items'][0]['added_at']); + $this->assertNotEmpty($wishlistResponse['items_v2']['items'][0]['configurable_options']); + $configurableOptions = $wishlistResponse['items_v2']['items'][0]['configurable_options']; $this->assertEquals('Test Configurable', $configurableOptions[0]['option_label']); + $this->assertEquals('Option 1', $configurableOptions[0]['value_label']); } /** @@ -138,8 +140,9 @@ private function getQuery( sharing_code items_count updated_at - items_v2 { - id + items_v2(currentPage:1,pageSize:1) { + items{ + id description quantity added_at @@ -153,6 +156,7 @@ private function getQuery( } } } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php index 0de45fb21b20b..901d1b2ee87cc 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php @@ -104,19 +104,20 @@ public function testAddDownloadableProductWithOptions(): void $this->assertArrayHasKey('addProductsToWishlist', $response); $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $this->assertEmpty($response['addProductsToWishlist']['user_errors']); $wishlistResponse = $response['addProductsToWishlist']['wishlist']; $this->assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); $this->assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); $this->assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); - $this->assertEquals($wishlistItem->getId(), $wishlistResponse['items_v2'][0]['id']); - $this->assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items_v2'][0]['quantity']); - $this->assertEquals($wishlistItem->getDescription(), $wishlistResponse['items_v2'][0]['description']); - $this->assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items_v2'][0]['added_at']); - $this->assertNotEmpty($wishlistResponse['items_v2'][0]['links_v2']); - $wishlistItemLinks = $wishlistResponse['items_v2'][0]['links_v2']; + $this->assertEquals($wishlistItem->getId(), $wishlistResponse['items_v2']['items'][0]['id']); + $this->assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items_v2']['items'][0]['quantity']); + $this->assertEquals($wishlistItem->getDescription(), $wishlistResponse['items_v2']['items'][0]['description']); + $this->assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items_v2']['items'][0]['added_at']); + $this->assertNotEmpty($wishlistResponse['items_v2']['items'][0]['links_v2']); + $wishlistItemLinks = $wishlistResponse['items_v2']['items'][0]['links_v2']; $this->assertEquals('Downloadable Product Link 1', $wishlistItemLinks[0]['title']); - $this->assertNotEmpty($wishlistResponse['items_v2'][0]['samples']); - $wishlistItemSamples = $wishlistResponse['items_v2'][0]['samples']; + $this->assertNotEmpty($wishlistResponse['items_v2']['items'][0]['samples']); + $wishlistItemSamples = $wishlistResponse['items_v2']['items'][0]['samples']; $this->assertEquals('Downloadable Product Sample', $wishlistItemSamples[0]['title']); } @@ -196,8 +197,10 @@ private function getQuery( sharing_code items_count updated_at - items_v2 { - id + items_v2(currentPage:1 pageSize:1) { + items + { + id description quantity added_at @@ -213,6 +216,7 @@ private function getQuery( sample_url } } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistsTest.php index e452e70c24148..6ce4388877825 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistsTest.php @@ -64,7 +64,7 @@ public function testCustomerWishlist(): void $this->assertEquals($wishlistItem->getItemsCount(), $wishlist['items_count']); $this->assertEquals($wishlistItem->getSharingCode(), $wishlist['sharing_code']); $this->assertEquals($wishlistItem->getUpdatedAt(), $wishlist['updated_at']); - $wishlistItemResponse = $wishlist['items_v2'][0]; + $wishlistItemResponse = $wishlist['items_v2']['items'][0]; $this->assertEquals('simple', $wishlistItemResponse['product']['sku']); } @@ -113,8 +113,7 @@ private function getQuery(): string sharing_code updated_at items_v2 { - product { - sku + items {product {name sku} } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php index dd7a54cff32a0..344b4e0f93d0d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php @@ -40,19 +40,21 @@ protected function setUp(): void public function testDeleteWishlistItemFromWishlist(): void { $wishlist = $this->getWishlist(); - $wishlistId = $wishlist['customer']['wishlist']['id']; - $wishlist = $wishlist['customer']['wishlist']; - $wishlistItems = $wishlist['items_v2']; - $this->assertEquals(1, $wishlist['items_count']); + $customerWishlists = $wishlist['customer']['wishlists'][0]; + $wishlistId = $customerWishlists['id']; - $query = $this->getQuery((int) $wishlistId, (int) $wishlistItems[0]['id']); + $wishlistItems = $customerWishlists['items_v2']['items']; + $this->assertEquals(1, $customerWishlists['items_count']); + + $query = $this->getQuery($wishlistId, $wishlistItems[0]['id']); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); $this->assertArrayHasKey('removeProductsFromWishlist', $response); $this->assertArrayHasKey('wishlist', $response['removeProductsFromWishlist']); + $this->assertEmpty($response['removeProductsFromWishlist']['user_errors'], 'User error is not empty'); $wishlistResponse = $response['removeProductsFromWishlist']['wishlist']; $this->assertEquals(0, $wishlistResponse['items_count']); - $this->assertEmpty($wishlistResponse['items_v2']); + $this->assertEmpty($wishlistResponse['items_v2']['items'], 'Wishlist item is not removed'); } /** @@ -64,10 +66,10 @@ public function testDeleteWishlistItemFromWishlist(): void public function testUnauthorizedWishlistItemDelete() { $wishlist = $this->getWishlist(); - $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $wishlistItem = $wishlist['customer']['wishlists'][0]['items_v2']['items']; $wishlist2 = $this->getWishlist('customer_two@example.com'); - $wishlist2Id = $wishlist2['customer']['wishlist']['id']; - $query = $this->getQuery((int) $wishlist2Id, (int) $wishlistItem['id']); + $wishlist2Id = $wishlist2['customer']['wishlists'][0]['id']; + $query = $this->getQuery($wishlist2Id, $wishlistItem[0]['id']); $response = $this->graphQlMutation( $query, [], @@ -75,10 +77,10 @@ public function testUnauthorizedWishlistItemDelete() $this->getHeaderMap('customer_two@example.com') ); self::assertEquals(1, $response['removeProductsFromWishlist']['wishlist']['items_count']); - self::assertNotEmpty($response['removeProductsFromWishlist']['wishlist']['items_v2'], 'empty wish list items'); - self::assertCount(1, $response['removeProductsFromWishlist']['wishlist']['items_v2']); + self::assertNotEmpty($response['removeProductsFromWishlist']['wishlist']['items_v2']['items'], 'empty wish list items'); + self::assertCount(1, $response['removeProductsFromWishlist']['wishlist']['items_v2']['items']); self::assertEquals( - 'The wishlist item with ID "'.$wishlistItem['id'].'" does not belong to the wishlist', + 'The wishlist item with ID "' . $wishlistItem[0]['id'] . '" does not belong to the wishlist', $response['removeProductsFromWishlist']['user_errors'][0]['message'] ); } @@ -109,14 +111,14 @@ private function getHeaderMap(string $username = 'customer@example.com', string * @return string */ private function getQuery( - int $wishlistId, - int $wishlistItemId + string $wishlistId, + string $wishlistItemId ): string { return <<getWishlist(); $qty = 5; $description = 'New Description'; - $wishlistId = $wishlist['customer']['wishlist']['id']; - $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $customerWishlist = $wishlist['customer']['wishlists'][0]; + $wishlistId = $customerWishlist['id']; + $wishlistItem = $customerWishlist['items_v2']['items'][0]; $this->assertNotEquals($description, $wishlistItem['description']); $this->assertNotEquals($qty, $wishlistItem['quantity']); - $query = $this->getQuery((int) $wishlistId, (int) $wishlistItem['id'], $qty, $description); + $query = $this->getQuery($wishlistId, $wishlistItem['id'], $qty, $description); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); $this->assertArrayHasKey('updateProductsInWishlist', $response); $this->assertArrayHasKey('wishlist', $response['updateProductsInWishlist']); + $this->assertEmpty($response['updateProductsInWishlist']['user_errors']); $wishlistResponse = $response['updateProductsInWishlist']['wishlist']; - $this->assertEquals($qty, $wishlistResponse['items_v2'][0]['quantity']); - $this->assertEquals($description, $wishlistResponse['items_v2'][0]['description']); + $this->assertEquals($qty, $wishlistResponse['items_v2']['items'][0]['quantity']); + $this->assertEquals($description, $wishlistResponse['items_v2']['items'][0]['description']); } /** @@ -67,12 +69,13 @@ public function testUpdateSimpleProductFromWishlist(): void public function testUnauthorizedWishlistItemUpdate() { $wishlist = $this->getWishlist(); - $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $customerWishlist = $wishlist['customer']['wishlists'][0]; + $wishlistItem = $customerWishlist['items_v2']['items'][0]; $wishlist2 = $this->getWishlist('customer_two@example.com'); - $wishlist2Id = $wishlist2['customer']['wishlist']['id']; + $wishlist2Id = $wishlist2['customer']['wishlists'][0]['id']; $qty = 2; $description = 'New Description'; - $updateWishlistQuery = $this->getQuery((int) $wishlist2Id, (int) $wishlistItem['id'], $qty, $description); + $updateWishlistQuery = $this->getQuery($wishlist2Id, $wishlistItem['id'], $qty, $description); $response = $this->graphQlMutation( $updateWishlistQuery, [], @@ -82,8 +85,9 @@ public function testUnauthorizedWishlistItemUpdate() self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items_v2'], 'empty wish list items'); self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items_v2']); + self::assertNotEmpty($response['updateProductsInWishlist']['user_errors'], 'No user errors'); self::assertEquals( - 'The wishlist item with ID "'.$wishlistItem['id'].'" does not belong to the wishlist', + 'The wishlist item with ID "' . $wishlistItem['id'] . '" does not belong to the wishlist', $response['updateProductsInWishlist']['user_errors'][0]['message'] ); } @@ -98,11 +102,12 @@ public function testUnauthorizedWishlistItemUpdate() public function testUpdateProductInWishlistWithZeroQty() { $wishlist = $this->getWishlist(); - $wishlistId = $wishlist['customer']['wishlist']['id']; - $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $customerWishlist = $wishlist['customer']['wishlists'][0]; + $wishlistId = $customerWishlist['id']; + $wishlistItem = $customerWishlist['items_v2']['items'][0]; $qty = 0; $description = 'Description for zero quantity'; - $updateWishlistQuery = $this->getQuery((int) $wishlistId, (int) $wishlistItem['id'], $qty, $description); + $updateWishlistQuery = $this->getQuery($wishlistId, $wishlistItem['id'], $qty, $description); $response = $this->graphQlMutation($updateWishlistQuery, [], '', $this->getHeaderMap()); self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items_v2'], 'empty wish list items'); @@ -126,16 +131,17 @@ public function testUpdateProductInWishlistWithZeroQty() public function testUpdateProductWithValidQtyAndNoDescription() { $wishlist = $this->getWishlist(); - $wishlistId = $wishlist['customer']['wishlist']['id']; - $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $customerWishlist = $wishlist['customer']['wishlists'][0]; + $wishlistId = $customerWishlist['id']; + $wishlistItem = $customerWishlist['items_v2']['items'][0]; $qty = 2; - $updateWishlistQuery = $this->getQueryWithNoDescription((int) $wishlistId, (int) $wishlistItem['id'], $qty); + $updateWishlistQuery = $this->getQueryWithNoDescription($wishlistId, $wishlistItem['id'], $qty); $response = $this->graphQlMutation($updateWishlistQuery, [], '', $this->getHeaderMap()); self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); - self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items'], 'empty wish list items'); - self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items']); - $itemsInWishlist = $response['updateProductsInWishlist']['wishlist']['items'][0]; - self::assertEquals($qty, $itemsInWishlist['qty']); + self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items_v2'], 'empty wish list items'); + self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items_v2']['items']); + $itemsInWishlist = $response['updateProductsInWishlist']['wishlist']['items_v2']['items'][0]; + self::assertEquals($qty, $itemsInWishlist['quantity']); self::assertEquals('simple-1', $itemsInWishlist['product']['sku']); } @@ -167,15 +173,15 @@ private function getHeaderMap(string $username = 'customer@example.com', string * @return string */ private function getQuery( - int $wishlistId, - int $wishlistItemId, + string $wishlistId, + string $wishlistItemId, int $qty, string $description ): string { return <<get(ConfigInterface::class); +$config->saveConfig('catalog/seo/product_url_suffix', null); +$config->saveConfig('catalog/seo/category_url_suffix', null); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); + +Resolver::getInstance()->requireDataFixture('Magento/CatalogUrlRewrite/_files/product_with_category.php'); diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix_rollback.php new file mode 100644 index 0000000000000..5cf753e04ca7b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix_rollback.php @@ -0,0 +1,19 @@ +get(ConfigInterface::class); +$config->deleteConfig('catalog/seo/product_url_suffix'); +$config->deleteConfig('catalog/seo/category_url_suffix'); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); + +Resolver::getInstance()->requireDataFixture('Magento/CatalogUrlRewrite/_files/product_with_category_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php index e9fa6d5bf96b7..20d366d05ac4a 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php @@ -57,30 +57,69 @@ protected function setUp(): void */ public function testGetRelationsByChildren(): void { - // Find configurable products options - $productOptionSkus = ['simple_10', 'simple_20', 'simple_30', 'simple_40']; - $searchCriteria = $this->searchCriteriaBuilder->addFilter('sku', $productOptionSkus, 'in') + $childSkusOfParentSkus = [ + 'configurable' => ['simple_10', 'simple_20'], + 'configurable_12345' => ['simple_30', 'simple_40'], + ]; + $configurableSkus = [ + 'configurable', + 'configurable_12345', + 'simple_10', + 'simple_20', + 'simple_30', + 'simple_40', + ]; + $configurableIdsOfSkus = []; + + $searchCriteria = $this->searchCriteriaBuilder->addFilter('sku', $configurableSkus, 'in') ->create(); - $productOptions = $this->productRepository->getList($searchCriteria) + $configurableProducts = $this->productRepository->getList($searchCriteria) ->getItems(); - $productOptionsIds = []; + $childIds = []; - foreach ($productOptions as $productOption) { - $productOptionsIds[] = $productOption->getId(); + foreach ($configurableProducts as $product) { + $configurableIdsOfSkus[$product->getSku()] = $product->getId(); + + if ($product->getTypeId() != 'configurable') { + $childIds[] = $product->getId(); + } } - // Find configurable products - $searchCriteria = $this->searchCriteriaBuilder->addFilter('sku', ['configurable', 'configurable_12345'], 'in') - ->create(); - $configurableProducts = $this->productRepository->getList($searchCriteria) - ->getItems(); + $parentIdsOfChildIds = []; + + foreach ($childSkusOfParentSkus as $parentSku => $childSkus) { + foreach ($childSkus as $childSku) { + $childId = $configurableIdsOfSkus[$childSku]; + $parentIdsOfChildIds[$childId][] = $configurableIdsOfSkus[$parentSku]; + } + } - // Assert there are configurable products ids in result of getRelationsByChildren method. - $result = $this->model->getRelationsByChildren($productOptionsIds); + /** + * Assert there are parent configurable products ids in result of getRelationsByChildren method + * and they are related to child ids. + */ + $result = $this->model->getRelationsByChildren($childIds); + $sortedResult = $this->sortParentIdsOfChildIds($result); + $sortedExpected = $this->sortParentIdsOfChildIds($parentIdsOfChildIds); - foreach ($configurableProducts as $configurableProduct) { - $this->assertContains($configurableProduct->getId(), $result); + $this->assertEquals($sortedExpected, $sortedResult); + } + + /** + * Sorts the "Parent Ids Of Child Ids" type of the array + * + * @param array $array + * @return array + */ + private function sortParentIdsOfChildIds(array $array): array + { + foreach ($array as $childId => &$parentIds) { + sort($parentIds, SORT_NUMERIC); } + + ksort($array, SORT_NUMERIC); + + return $array; } } diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php index 038a8c7255815..72a093dc48e79 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php @@ -139,4 +139,33 @@ public function testDifferentProductsRequestsUseDifferentPageCacheRecords(): voi $this->assertEquals('MISS', $responseProduct1->getHeader('X-Magento-Cache-Debug')->getFieldValue()); $this->assertEquals('MISS', $responseProduct2->getHeader('X-Magento-Cache-Debug')->getFieldValue()); } + + /** + * Test response has category tags when products are filtered by category id + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testProductsFilterByCategoryHasCategoryTags(): void + { + $query + = <<dispatchGraphQlGETRequest(['query' => $query]); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cat_c', 'cat_c_333']; + + foreach ($expectedCacheTags as $cacheTag) { + $this->assertContains($cacheTag, $actualCacheTags); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin.php b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin.php new file mode 100755 index 0000000000000..66acb3c70f9be --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin.php @@ -0,0 +1,53 @@ +get(RoleFactory::class)->create(); +$role->setName('test_custom_role'); +$role->setData('role_name', $role->getName()); +$role->setRoleType(\Magento\Authorization\Model\Acl\Role\Group::ROLE_TYPE); +$role->setUserType((string)\Magento\Authorization\Model\UserContextInterface::USER_TYPE_ADMIN); + +/** @var RoleResource $roleResource */ +$roleResource = Bootstrap::getObjectManager()->get(RoleResource::class); +$roleResource->save($role); + +/** @var Rules $rules */ +$rules = Bootstrap::getObjectManager()->get(RulesFactory::class)->create(); +$rules->setRoleId($role->getId()); +//Granted all permissions. +$rules->setResources([Bootstrap::getObjectManager()->get(\Magento\Framework\Acl\RootResource::class)->getId()]); + +/** @var RulesResource $rulesResource */ +$rulesResource = Bootstrap::getObjectManager()->get(RulesResource::class); +$rulesResource->saveRel($rules); + +/** @var User $user */ +$user = Bootstrap::getObjectManager()->create(User::class); +$user->setFirstname("John") + ->setLastname("Doe") + ->setUsername('TestAdmin1') + ->setPassword(\Magento\TestFramework\Bootstrap::ADMIN_PASSWORD) + ->setEmail('testadmin1@gmail.com') + ->setIsActive(1) + ->setRoleId($role->getId()); + +/** @var UserResource $userResource */ +$userResource = Bootstrap::getObjectManager()->get(UserResource::class); +$userResource->save($user); \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin_rollback.php b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin_rollback.php new file mode 100755 index 0000000000000..aabfca018d974 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin_rollback.php @@ -0,0 +1,42 @@ +create(User::class); +$user->load('TestAdmin1', 'username'); + +/** @var UserResource $userResource */ +$userResource = Bootstrap::getObjectManager()->get(UserResource::class); +$userResource->delete($user); + +/** @var Role $role */ +$role = Bootstrap::getObjectManager()->get(RoleFactory::class)->create(); +$role->load('test_custom_role', 'role_name'); + +/** @var Rules $rules */ +$rules = Bootstrap::getObjectManager()->get(RulesFactory::class)->create(); +$rules->load($role->getId(), 'role_id'); + +/** @var RulesResource $rulesResource */ +$rulesResource = Bootstrap::getObjectManager()->get(RulesResource::class); +$rulesResource->delete($rules); + +/** @var RoleResource $roleResource */ +$roleResource = Bootstrap::getObjectManager()->get(RoleResource::class); +$roleResource->delete($role); diff --git a/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/customer.php b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/customer.php new file mode 100644 index 0000000000000..dba02bc340738 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/customer.php @@ -0,0 +1,48 @@ +create(\Magento\Customer\Api\CustomerRepositoryInterface::class); +$customer = $objectManager->create(\Magento\Customer\Model\Customer::class); + +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +/** @var Magento\Customer\Model\Customer $customer */ +$customer->setWebsiteId(1) + ->setId(1) + ->setEmail('customer@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setTaxvat('12') + ->setGender(0); + +$extension = $customer->getExtensionAttributes(); +if ($extension === null) { + $extension = $objectManager->get(CustomerExtensionFactory::class)->create(); +} + +$extension->setAssistanceAllowed(2); +$customer->setExtensionAttributes($extension); + +$customer->isObjectNew(true); +$customer->save(); + +$customerRegistry->remove($customer->getId()); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/composer_module_names.txt b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/composer_module_names.txt new file mode 100644 index 0000000000000..84ce58249ae36 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/composer_module_names.txt @@ -0,0 +1 @@ +magento/module-aws-s3 diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/ArgumentsCompositeProcessor.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/ArgumentsCompositeProcessor.php new file mode 100644 index 0000000000000..8515164e4323e --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/ArgumentsCompositeProcessor.php @@ -0,0 +1,52 @@ +processors = $processors; + } + + /** + * Composite processor that loops through available processors for arguments that come from graphql input + * + * @param string $fieldName, + * @param array $args + * @return array + * @throws GraphQlInputException + */ + public function process( + string $fieldName, + array $args + ): array { + $processedArgs = $args; + foreach ($this->processors as $processor) { + $processedArgs = $processor->process( + $fieldName, + $processedArgs + ); + } + + return $processedArgs; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/ArgumentsProcessorInterface.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/ArgumentsProcessorInterface.php new file mode 100644 index 0000000000000..212211c349fd5 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/ArgumentsProcessorInterface.php @@ -0,0 +1,29 @@ +