diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml deleted file mode 100644 index e02c34fd8868e..0000000000000 --- a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - <description value="Checks to see if the tracking script is in the dom of admin and if setting is turned to no it checks if the tracking script in the dom was removed"/> - <severity value="CRITICAL"/> - <testCaseId value="MC-18192"/> - <group value="backend"/> - <group value="login"/> - </annotations> - - <!-- Logging in Magento admin --> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - </test> -</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php index cf12e332be86d..8da2614eb2ced 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php @@ -261,6 +261,10 @@ public function execute() unset($data['apply_to']); } + if ($model->getBackendType() == 'static' && !$model->getIsUserDefined()) { + $data['frontend_class'] = $model->getFrontendClass(); + } + $model->addData($data); if (!$attributeId) { diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php index 77dedb9eb0121..3494fd00a8b6c 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php @@ -72,6 +72,8 @@ public function renderRangeLabel($fromPrice, $toPrice) } /** + * Prepare range data + * * @param int $range * @param int[] $dbRanges * @return array @@ -81,12 +83,10 @@ public function renderRangeData($range, $dbRanges) if (empty($dbRanges)) { return []; } - $lastIndex = array_keys($dbRanges); - $lastIndex = $lastIndex[count($lastIndex) - 1]; foreach ($dbRanges as $index => $count) { - $fromPrice = $index == 1 ? '' : ($index - 1) * $range; - $toPrice = $index == $lastIndex ? '' : $index * $range; + $fromPrice = $index == 1 ? 0 : ($index - 1) * $range; + $toPrice = $index * $range; $this->itemDataBuilder->addItemData( $this->renderRangeLabel($fromPrice, $toPrice), $fromPrice . '-' . $toPrice, diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php index fab2441db26c9..939f9d354af85 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php @@ -8,11 +8,15 @@ use Magento\Catalog\Model\Category; /** + * Aggregate count for parent category after deleting child category + * * Class AggregateCount */ class AggregateCount { /** + * Reduces children count for parent categories + * * @param Category $category * @return void */ @@ -25,9 +29,7 @@ public function processDelete(Category $category) */ $parentIds = $category->getParentIds(); if ($parentIds) { - $childDecrease = $category->getChildrenCount() + 1; - // +1 is itself - $data = ['children_count' => new \Zend_Db_Expr('children_count - ' . $childDecrease)]; + $data = ['children_count' => new \Zend_Db_Expr('children_count - 1')]; $where = ['entity_id IN(?)' => $parentIds]; $resourceModel->getConnection()->update($resourceModel->getEntityTable(), $data, $where); } diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml new file mode 100644 index 0000000000000..bd7eb664819dd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnableCategoryActionGroup"> + <annotations> + <description>Enable the category</description> + </annotations> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="enableCategory"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml new file mode 100644 index 0000000000000..3a75b0a3cd361 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminCategoryIsListedInCategoriesTreeActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="seeCategoryInTree"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml new file mode 100644 index 0000000000000..e0a98a8932d4d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="doNotSeeCategoryInTree"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml new file mode 100644 index 0000000000000..4a403364a91e3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSwitchStoreActionGroup"> + <annotations> + <description>Switch the Storefront to the provided Store.</description> + </annotations> + <arguments> + <argument name="storeName" type="string"/> + </arguments> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickOnSwitchStoreButton"/> + <click selector="{{StorefrontFooterSection.storeLink(storeName)}}" stepKey="selectStoreToSwitchOn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml index 192bab7c6d126..2cdec1405e9f9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml @@ -31,21 +31,19 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create subcategory under parent category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedCategory"> + <argument name="Category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> <!-- Verify Parent Category is visible in navigation menu and Sub category is not visible in navigation menu --> - <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> - <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnStoreNavigationBar"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCategoryOnStoreNavigationBar"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeSubCategoryOnStoreNavigation"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml index 4b0774d2307dd..e66984dda4427 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml @@ -21,10 +21,8 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> </before> <after> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> - <waitForPageLoad stepKey="waitStoreIndexPageLoad" /> <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> - <argument name="storeGroupName" value="customStore.name"/> + <argument name="storeGroupName" value="customStoreGroup.name"/> </actionGroup> <actionGroup ref="DeleteCategoryActionGroup" stepKey="deleteCreatedNewRootCategory"> <argument name="categoryEntity" value="NewRootCategory"/> @@ -37,39 +35,32 @@ <argument name="categoryEntity" value="NewRootCategory"/> </actionGroup> <!--Create subcategory--> - <scrollToTopOfPage stepKey="scrollToTopOfPage"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name)}}" stepKey="clickOnCreatedNewRootCategory"/> - <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedCategory"> + <argument name="Category" value="NewRootCategory"/> + </actionGroup> + <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory"> <argument name="categoryEntity" value="SimpleSubCategory"/> </actionGroup> <!--Create a Store--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Create a Store View--> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> - <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> - <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> - <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> - <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> <!--Go to store front page--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> <!--Verify subcategory displayed in store front page--> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectCustomStore"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeSubCategoryInStoreFrontPage"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="switchToCustomStore"> + <argument name="storeName" value="{{customStoreGroup.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml index 40bd3bdcfea20..4979b06a1051e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml @@ -27,16 +27,19 @@ <!--Verify Created root Category--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminCategoryBasicFieldSection.CategoryNameInput(NewRootCategory.name)}}" stepKey="seeRootCategory"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsListedInCategoriesTreeActionGroup" stepKey="seeRootCategory"> + <argument name="categoryName" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Delete Root Category--> <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> <!--Verify Root Category is not listed in backend--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories1"/> - <dontSee selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{NewRootCategory.name}}" stepKey="dontSeeRootCategory"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandTheCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup" stepKey="doNotSeeRootCategory"> + <argument name="categoryName" value="{{NewRootCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml index fe07360d6b9ca..4310c6f06219a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml @@ -33,59 +33,48 @@ </after> <!--Create a Store--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> - <see userInput="You saved the store." stepKey="seeSaveMessage"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStore.name}}"/> + <argument name="rootCategory" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Create a Store View--> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> - <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> - <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> - <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> - <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> - <see userInput="You saved the store view." stepKey="seeSaveMessage1"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="customStore"/> + </actionGroup> <!--Go To store front page--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> <!--Verify subcategory displayed in store front--> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectMainWebsite1"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeSubCategoryInStoreFront"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="selectCustomStore"> + <argument name="storeName" value="{{customStore.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryInStoreFront"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Delete SubCategory--> <deleteData createDataKey="category" stepKey="deleteCategory"/> <!--Verify Sub Category is absent in backend --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories2"/> - <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="dontSeeCategoryInTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandTheCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup" stepKey="doNotSeeRootCategory"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Verify Sub Category is not present in Store Front--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad2"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryInStoreFront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeOldCategoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> <!--Verify in Category is not in Url Rewrite grid--> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> - <waitForPageLoad stepKey="waitForUrlRewritePageTopLoad"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillRequestPath"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="seeEmptyRow"/> + <actionGroup ref="AdminSearchDeletedUrlRewriteActionGroup" stepKey="searchingCategoryUrlRewrite"> + <argument name="requestPath" value="{{SimpleRootSubCategory.url_key}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml index 9b5fa25085e1a..48e6245b011ba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml @@ -37,23 +37,26 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront2"/> - <waitForPageLoad stepKey="waitForCategoryStorefront"/> - <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="dontSeeCreatedProduct"/> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="onCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandAll"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$simpleSubCategory.name$$)}}" stepKey="clickOnCreatedSimpleSubCategoryBeforeDelete"/> - <waitForPageLoad stepKey="AdminCategoryEditPageLoad"/> - <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="EnableCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AssertStorefrontProductAbsentOnCategoryPageActionGroup" stepKey="doNotSeeProductOnCategoryPage"> + <argument name="categoryUrlKey" value="$$createCategory.name$$"/> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedSubCategory"> + <argument name="Category" value="$$simpleSubCategory$$"/> + </actionGroup> + <actionGroup ref="AdminEnableCategoryActionGroup" stepKey="enableCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="seeSuccessMessage"/> <!--Run re-index task--> <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront"/> - <waitForPageLoad stepKey="waitForCategoryStorefrontPage"/> - <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="seeCreatedProduct"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openEnabledCategory"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="seeCreatedProduct"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php index ca35d49113f41..681cef8489796 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php @@ -7,10 +7,12 @@ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; +use Magento\Backend\Model\Session; use Magento\Backend\Model\View\Result\Redirect as ResultRedirect; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save; use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Catalog\Model\Product\Attribute\Frontend\Inputtype\Presentation; use Magento\Catalog\Model\Product\AttributeSet\Build; use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; @@ -31,63 +33,64 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class SaveTest extends AttributeTest { /** * @var BuildFactory|MockObject */ - protected $buildFactoryMock; + private $buildFactoryMock; /** * @var FilterManager|MockObject */ - protected $filterManagerMock; + private $filterManagerMock; /** * @var ProductHelper|MockObject */ - protected $productHelperMock; + private $productHelperMock; /** * @var AttributeFactory|MockObject */ - protected $attributeFactoryMock; + private $attributeFactoryMock; /** * @var ValidatorFactory|MockObject */ - protected $validatorFactoryMock; + private $validatorFactoryMock; /** * @var CollectionFactory|MockObject */ - protected $groupCollectionFactoryMock; + private $groupCollectionFactoryMock; /** * @var LayoutFactory|MockObject */ - protected $layoutFactoryMock; + private $layoutFactoryMock; /** * @var ResultRedirect|MockObject */ - protected $redirectMock; + private $redirectMock; /** - * @var AttributeSet|MockObject + * @var AttributeSetInterface|MockObject */ - protected $attributeSetMock; + private $attributeSetMock; /** * @var Build|MockObject */ - protected $builderMock; + private $builderMock; /** * @var InputTypeValidator|MockObject */ - protected $inputTypeValidatorMock; + private $inputTypeValidatorMock; /** * @var FormData|MockObject @@ -104,19 +107,34 @@ class SaveTest extends AttributeTest */ private $attributeCodeValidatorMock; + /** + * @var Presentation|MockObject + */ + private $presentationMock; + + /** + * @var Session|MockObject + */ + + private $sessionMock; + protected function setUp(): void { parent::setUp(); + $this->filterManagerMock = $this->createMock(FilterManager::class); + $this->productHelperMock = $this->createMock(ProductHelper::class); + $this->attributeSetMock = $this->createMock(AttributeSetInterface::class); + $this->builderMock = $this->createMock(Build::class); + $this->inputTypeValidatorMock = $this->createMock(InputTypeValidator::class); + $this->formDataSerializerMock = $this->createMock(FormData::class); + $this->attributeCodeValidatorMock = $this->createMock(AttributeCodeValidator::class); + $this->presentationMock = $this->createMock(Presentation::class); + $this->sessionMock = $this->createMock(Session::class); + $this->layoutFactoryMock = $this->createMock(LayoutFactory::class); $this->buildFactoryMock = $this->getMockBuilder(BuildFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->filterManagerMock = $this->getMockBuilder(FilterManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productHelperMock = $this->getMockBuilder(ProductHelper::class) - ->disableOriginalConstructor() - ->getMock(); $this->attributeFactoryMock = $this->getMockBuilder(AttributeFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() @@ -129,32 +147,23 @@ protected function setUp(): void ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->layoutFactoryMock = $this->getMockBuilder(LayoutFactory::class) - ->disableOriginalConstructor() - ->getMock(); $this->redirectMock = $this->getMockBuilder(ResultRedirect::class) ->setMethods(['setData', 'setPath']) ->disableOriginalConstructor() ->getMock(); - $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->builderMock = $this->getMockBuilder(Build::class) - ->disableOriginalConstructor() - ->getMock(); - $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) - ->disableOriginalConstructor() - ->getMock(); - $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class) - ->disableOriginalConstructor() - ->getMock(); $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) - ->setMethods(['getId', 'get']) - ->getMockForAbstractClass(); - + ->setMethods( + [ + 'getId', + 'get', + 'getBackendTypeByInput', + 'getDefaultValueByInput', + 'getBackendType', + 'getFrontendClass', + 'addData', + 'save' + ] + )->getMockForAbstractClass(); $this->buildFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->builderMock); @@ -167,7 +176,7 @@ protected function setUp(): void } /** - * {@inheritdoc} + * @inheritdoc */ protected function getModel() { @@ -184,7 +193,9 @@ protected function getModel() 'groupCollectionFactory' => $this->groupCollectionFactoryMock, 'layoutFactory' => $this->layoutFactoryMock, 'formDataSerializer' => $this->formDataSerializerMock, - 'attributeCodeValidator' => $this->attributeCodeValidatorMock + 'attributeCodeValidator' => $this->attributeCodeValidatorMock, + 'presentation' => $this->presentationMock, + '_session' => $this->sessionMock ]); } @@ -214,6 +225,67 @@ public function testExecuteWithEmptyData() $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); } + public function testExecuteSaveFrontendClass() + { + $data = [ + 'frontend_input' => 'test_frontend_input', + ]; + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['isAjax', null, null], + ['serialized_options', '[]', ''], + ['set', null, 1], + ['attribute_code', null, 'test_attribute_code'], + ]); + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with('') + ->willReturn([]); + $this->requestMock->expects($this->once()) + ->method('getPostValue') + ->willReturn($data); + $this->inputTypeValidatorMock->expects($this->any()) + ->method('isValid') + ->with($data['frontend_input']) + ->willReturn(true); + $this->presentationMock->expects($this->once()) + ->method('convertPresentationDataToInputType') + ->willReturn($data); + $this->productHelperMock->expects($this->once()) + ->method('getAttributeSourceModelByInputType') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productHelperMock->expects($this->once()) + ->method('getAttributeBackendModelByInputType') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productAttributeMock->expects($this->once()) + ->method('getBackendTypeByInput') + ->with($data['frontend_input']) + ->willReturnSelf('test_backend_type'); + $this->productAttributeMock->expects($this->once()) + ->method('getDefaultValueByInput') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productAttributeMock->expects($this->once()) + ->method('getBackendType') + ->willReturn('static'); + $this->productAttributeMock->expects($this->once()) + ->method('getFrontendClass') + ->willReturn('static'); + $this->resultFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->redirectMock); + $this->redirectMock->expects($this->any()) + ->method('setPath') + ->willReturnSelf(); + + $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); + } + public function testExecute() { $data = [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php new file mode 100644 index 0000000000000..c73e02fb7ecbf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Category; + +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\ResourceModel\Category\AggregateCount; +use Magento\Catalog\Model\ResourceModel\Category as ResourceCategory; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Aggregate count model test + */ +class AggregateCountTest extends TestCase +{ + + /** + * @var AggregateCount + */ + protected $aggregateCount; + + /** + * @var ObjectManagerHelper + */ + protected $objectManagerHelper; + + /** + * @var Category|MockObject + */ + protected $categoryMock; + + /** + * @var ResourceCategory|MockObject + */ + protected $resourceCategoryMock; + + /** + * @var AdapterInterface|MockObject + */ + protected $connectionMock; + + /** + * {@inheritdoc} + */ + public function setUp(): void + { + $this->categoryMock = $this->createMock(Category::class); + $this->resourceCategoryMock = $this->createMock(ResourceCategory::class); + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class) + ->getMockForAbstractClass(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->aggregateCount = $this->objectManagerHelper->getObject(AggregateCount::class); + } + + /** + * @return void + */ + public function testProcessDelete(): void + { + $parentIds = 3; + $table = 'catalog_category_entity'; + + $this->categoryMock->expects($this->once()) + ->method('getResource') + ->willReturn($this->resourceCategoryMock); + $this->categoryMock->expects($this->once()) + ->method('getParentIds') + ->willReturn($parentIds); + $this->resourceCategoryMock->expects($this->any()) + ->method('getEntityTable') + ->willReturn($table); + $this->resourceCategoryMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + $this->connectionMock->expects($this->once()) + ->method('update') + ->with( + $table, + ['children_count' => new \Zend_Db_Expr('children_count - 1')], + ['entity_id IN(?)' => $parentIds] + ); + $this->aggregateCount->processDelete($this->categoryMock); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index 254d893d24584..17318d4207841 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -7,13 +7,16 @@ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; -use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Categories; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; +use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; use Magento\Framework\AuthorizationInterface; use Magento\Framework\DB\Helper as DbHelper; use Magento\Framework\UrlInterface; use Magento\Store\Model\Store; +use Magento\Backend\Model\Auth\Session; +use Magento\Authorization\Model\Role; +use Magento\User\Model\User; use PHPUnit\Framework\MockObject\MockObject; /** @@ -51,6 +54,11 @@ class CategoriesTest extends AbstractModifierTest */ private $authorizationMock; + /** + * @var Session|MockObject + */ + private $sessionMock; + protected function setUp(): void { parent::setUp(); @@ -72,7 +80,10 @@ protected function setUp(): void $this->authorizationMock = $this->getMockBuilder(AuthorizationInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - + $this->sessionMock = $this->getMockBuilder(Session::class) + ->setMethods(['getUser']) + ->disableOriginalConstructor() + ->getMock(); $this->categoryCollectionFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->categoryCollectionMock); @@ -88,6 +99,26 @@ protected function setUp(): void $this->categoryCollectionMock->expects($this->any()) ->method('getIterator') ->willReturn(new \ArrayIterator([])); + + $roleAdmin = $this->getMockBuilder(Role::class) + ->setMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $roleAdmin->expects($this->any()) + ->method('getId') + ->willReturn(0); + + $userAdmin = $this->getMockBuilder(User::class) + ->setMethods(['getRole']) + ->disableOriginalConstructor() + ->getMock(); + $userAdmin->expects($this->any()) + ->method('getRole') + ->willReturn($roleAdmin); + + $this->sessionMock->expects($this->any()) + ->method('getUser') + ->willReturn($userAdmin); } /** @@ -101,11 +132,28 @@ protected function createModel() 'locator' => $this->locatorMock, 'categoryCollectionFactory' => $this->categoryCollectionFactoryMock, 'arrayManager' => $this->arrayManagerMock, - 'authorization' => $this->authorizationMock + 'authorization' => $this->authorizationMock, + 'session' => $this->sessionMock ] ); } + /** + * @param object $object + * @param string $method + * @param array $args + * @return mixed + * @throws \ReflectionException + */ + private function invokeMethod($object, $method, $args = []) + { + $class = new \ReflectionClass(Categories::class); + $method = $class->getMethod($method); + $method->setAccessible(true); + + return $method->invokeArgs($object, $args); + } + public function testModifyData() { $this->assertSame([], $this->getModel()->modifyData([])); @@ -176,4 +224,44 @@ public function modifyMetaLockedDataProvider() { return [[true], [false]]; } + + /** + * Asserts that a user with an ACL role ID of 0 and a user with an ACL role ID of 1 do not have the same cache IDs + * Assumes a store ID of 0 + * + * @throws \ReflectionException + */ + public function testAclCacheIds() + { + $categoriesAdmin = $this->createModel(); + $cacheIdAdmin = $this->invokeMethod($categoriesAdmin, 'getCategoriesTreeCacheId', [0]); + + $roleAclUser = $this->getMockBuilder(Role::class) + ->disableOriginalConstructor() + ->getMock(); + $roleAclUser->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $userAclUser = $this->getMockBuilder(User::class) + ->disableOriginalConstructor() + ->getMock(); + $userAclUser->expects($this->any()) + ->method('getRole') + ->will($this->returnValue($roleAclUser)); + + $this->sessionMock = $this->getMockBuilder(Session::class) + ->setMethods(['getUser']) + ->disableOriginalConstructor() + ->getMock(); + + $this->sessionMock->expects($this->any()) + ->method('getUser') + ->will($this->returnValue($userAclUser)); + + $categoriesAclUser = $this->createModel(); + $cacheIdAclUser = $this->invokeMethod($categoriesAclUser, 'getCategoriesTreeCacheId', [0]); + + $this->assertNotEquals($cacheIdAdmin, $cacheIdAclUser); + } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php index 7608173c8edfc..c0d5f0a1af3b8 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php @@ -18,12 +18,14 @@ use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; use Magento\Framework\AuthorizationInterface; +use Magento\Backend\Model\Auth\Session; /** * Data provider for categories field of product page * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 101.0.0 */ class Categories extends AbstractModifier @@ -86,6 +88,11 @@ class Categories extends AbstractModifier */ private $authorization; + /** + * @var Session + */ + private $session; + /** * @param LocatorInterface $locator * @param CategoryCollectionFactory $categoryCollectionFactory @@ -94,6 +101,7 @@ class Categories extends AbstractModifier * @param ArrayManager $arrayManager * @param SerializerInterface $serializer * @param AuthorizationInterface $authorization + * @param Session $session */ public function __construct( LocatorInterface $locator, @@ -102,7 +110,8 @@ public function __construct( UrlInterface $urlBuilder, ArrayManager $arrayManager, SerializerInterface $serializer = null, - AuthorizationInterface $authorization = null + AuthorizationInterface $authorization = null, + Session $session = null ) { $this->locator = $locator; $this->categoryCollectionFactory = $categoryCollectionFactory; @@ -111,6 +120,7 @@ public function __construct( $this->arrayManager = $arrayManager; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); $this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class); + $this->session = $session ?: ObjectManager::getInstance()->get(Session::class); } /** @@ -370,10 +380,16 @@ protected function getCategoriesTree($filter = null) * @param string $filter * @return string */ - private function getCategoriesTreeCacheId(int $storeId, string $filter = '') : string + private function getCategoriesTreeCacheId(int $storeId, string $filter = ''): string { + if ($this->session->getUser() !== null) { + return self::CATEGORY_TREE_ID + . '_' . (string)$storeId + . '_' . $this->session->getUser()->getAclRole() + . '_' . $filter; + } return self::CATEGORY_TREE_ID - . '_' . (string) $storeId + . '_' . (string)$storeId . '_' . $filter; } diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index 69592657241a0..0bfd9d58ec969 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -8,7 +8,10 @@ namespace Magento\CatalogGraphQl\Model; use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\NodeKind; use Magento\Eav\Model\Entity\Collection\AbstractCollection; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Joins attributes for provided field node field names. @@ -43,11 +46,12 @@ public function __construct(array $fieldToAttributeMap = []) * * @param FieldNode $fieldNode * @param AbstractCollection $collection + * @param ResolveInfo $resolveInfo * @return void */ - public function join(FieldNode $fieldNode, AbstractCollection $collection): void + public function join(FieldNode $fieldNode, AbstractCollection $collection, ResolveInfo $resolveInfo): void { - foreach ($this->getQueryFields($fieldNode) as $field) { + foreach ($this->getQueryFields($fieldNode, $resolveInfo) as $field) { $this->addFieldToCollection($collection, $field); } } @@ -56,26 +60,70 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection): void * Get an array of queried fields. * * @param FieldNode $fieldNode + * @param ResolveInfo $resolveInfo * @return string[] */ - public function getQueryFields(FieldNode $fieldNode): array + public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): array { if (null === $this->getFieldNodeSelections($fieldNode)) { $query = $fieldNode->selectionSet->selections; $selectedFields = []; + $fragmentFields = []; /** @var FieldNode $field */ foreach ($query as $field) { - if ($field->kind === 'InlineFragment') { - continue; + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field); + } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD && + ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) { + + foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) { + if (isset($spreadNode->selectionSet->selections)) { + $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo); + } else { + $selectedFields[] = $spreadNode->name->value; + } + } + } else { + $selectedFields[] = $field->name->value; } - $selectedFields[] = $field->name->value; } - $this->setSelectionsForFieldNode($fieldNode, $selectedFields); + if ($fragmentFields) { + $selectedFields = array_merge($selectedFields, array_merge(...$fragmentFields)); + } + $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields)); } return $this->getFieldNodeSelections($fieldNode); } + /** + * Add fields from inline fragment nodes + * + * @param ResolveInfo $resolveInfo + * @param InlineFragmentNode $inlineFragmentField + * @param array $inlineFragmentFields + * @return string[] + */ + private function addInlineFragmentFields( + ResolveInfo $resolveInfo, + InlineFragmentNode $inlineFragmentField, + $inlineFragmentFields = [] + ): array { + $query = $inlineFragmentField->selectionSet->selections; + /** @var FieldNode $field */ + foreach ($query as $field) { + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $this->addInlineFragmentFields($resolveInfo, $field, $inlineFragmentFields); + } elseif (isset($field->selectionSet->selections)) { + continue; + } else { + $inlineFragmentFields[] = $field->name->value; + } + } + + return array_unique($inlineFragmentFields); + } + /** * Add field to collection select * diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php index b5d02511da4e7..ab100c7272ba0 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php @@ -8,6 +8,9 @@ namespace Magento\CatalogGraphQl\Model\Category; use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\NodeKind; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Used for determining the depth information for a requested category tree in a GraphQL request @@ -17,22 +20,57 @@ class DepthCalculator /** * Calculate the total depth of a category tree inside a GraphQL request * + * @param ResolveInfo $resolveInfo * @param FieldNode $fieldNode * @return int */ - public function calculate(FieldNode $fieldNode) : int + public function calculate(ResolveInfo $resolveInfo, FieldNode $fieldNode) : int { $selections = $fieldNode->selectionSet->selections ?? []; $depth = count($selections) ? 1 : 0; $childrenDepth = [0]; foreach ($selections as $node) { - if ($node->kind === 'InlineFragment' || null !== $node->alias) { + if (isset($node->alias) && null !== $node->alias) { continue; } - $childrenDepth[] = $this->calculate($node); + if ($node->kind === NodeKind::INLINE_FRAGMENT) { + $childrenDepth[] = $this->addInlineFragmentDepth($resolveInfo, $node); + } elseif ($node->kind === NodeKind::FRAGMENT_SPREAD && isset($resolveInfo->fragments[$node->name->value])) { + foreach ($resolveInfo->fragments[$node->name->value]->selectionSet->selections as $spreadNode) { + $childrenDepth[] = $this->calculate($resolveInfo, $spreadNode); + } + } else { + $childrenDepth[] = $this->calculate($resolveInfo, $node); + } } return $depth + max($childrenDepth); } + + /** + * Add inline fragment fields into calculating of category depth + * + * @param ResolveInfo $resolveInfo + * @param InlineFragmentNode $inlineFragmentField + * @param array $depth + * @return int + */ + private function addInlineFragmentDepth( + ResolveInfo $resolveInfo, + InlineFragmentNode $inlineFragmentField, + $depth = [] + ): int { + $selections = $inlineFragmentField->selectionSet->selections; + /** @var FieldNode $field */ + foreach ($selections as $field) { + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $depth[] = $this->addInlineFragmentDepth($resolveInfo, $field, $depth); + } elseif ($field->selectionSet && $field->selectionSet->selections) { + $depth[] = $this->calculate($resolveInfo, $field); + } + } + + return $depth ? max($depth) : 0; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php index 5a230ceed0ca4..c6de07bdedd19 100644 --- a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php +++ b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php @@ -10,7 +10,7 @@ use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class ProductLinksTypeResolver implements TypeResolverInterface { @@ -20,9 +20,9 @@ class ProductLinksTypeResolver implements TypeResolverInterface private $linkTypes = ['related', 'upsell', 'crosssell']; /** - * {@inheritdoc} + * @inheritdoc */ - public function resolveType(array $data) : string + public function resolveType(array $data): string { if (isset($data['link_type'])) { $linkType = $data['link_type']; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php index 535fe3a80cd25..d7118d71db89b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php @@ -7,18 +7,18 @@ namespace Magento\CatalogGraphQl\Model\Resolver; -use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; +use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; +use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; -use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Store\Model\StoreManagerInterface; /** @@ -121,7 +121,7 @@ function () use ($that, $categoryIds, $info) { } if (!$this->collection->isLoaded()) { - $that->attributesJoiner->join($info->fieldNodes[0], $this->collection); + $that->attributesJoiner->join($info->fieldNodes[0], $this->collection, $info); $this->collection->addIdFilter($this->categoryIds); } /** @var CategoryInterface | \Magento\Catalog\Model\Category $item */ @@ -130,7 +130,7 @@ function () use ($that, $categoryIds, $info) { // Try to extract all requested fields from the loaded collection data $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item, true); $categories[$item->getId()]['model'] = $item; - $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0]); + $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0], $info); $extractedFields = array_keys($categories[$item->getId()]); $foundFields = array_intersect($requestedFields, $extractedFields); if (count($requestedFields) === count($foundFields)) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php index 14732ecf37c63..187fd05c1001e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php @@ -22,7 +22,15 @@ class BatchProductLinks implements BatchServiceContractResolverInterface /** * @var string[] */ - private static $linkTypes = ['related', 'upsell', 'crosssell']; + private $linkTypes; + + /** + * @param array $linkTypes + */ + public function __construct(array $linkTypes) + { + $this->linkTypes = $linkTypes; + } /** * @inheritDoc @@ -44,7 +52,7 @@ public function convertToServiceArgument(ResolveRequestInterface $request) /** @var \Magento\Catalog\Model\Product $product */ $product = $value['model']; - return new ListCriteria((string)$product->getId(), self::$linkTypes, $product); + return new ListCriteria((string)$product->getId(), $this->linkTypes, $product); } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php index 9ddad4e6451fa..3139c35774008 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use GraphQL\Language\AST\NodeKind; use Magento\Framework\GraphQl\Query\FieldTranslator; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -43,9 +44,9 @@ public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeN continue; } foreach ($node->selectionSet->selections as $selectionNode) { - if ($selectionNode->kind === 'InlineFragment') { + if ($selectionNode->kind === NodeKind::INLINE_FRAGMENT) { foreach ($selectionNode->selectionSet->selections as $inlineSelection) { - if ($inlineSelection->kind === 'InlineFragment') { + if ($inlineSelection->kind === NodeKind::INLINE_FRAGMENT) { continue; } $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php index fc5a563c82b4e..c553d4486f9e9 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php @@ -8,15 +8,16 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; use GraphQL\Language\AST\FieldNode; -use Magento\CatalogGraphQl\Model\Category\DepthCalculator; -use Magento\CatalogGraphQl\Model\Category\LevelCalculator; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use GraphQL\Language\AST\NodeKind; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; -use Magento\Catalog\Model\Category; +use Magento\CatalogGraphQl\Model\Category\DepthCalculator; +use Magento\CatalogGraphQl\Model\Category\LevelCalculator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Category tree data provider @@ -85,8 +86,8 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato { $categoryQuery = $resolveInfo->fieldNodes[0]; $collection = $this->collectionFactory->create(); - $this->joinAttributesRecursively($collection, $categoryQuery); - $depth = $this->depthCalculator->calculate($categoryQuery); + $this->joinAttributesRecursively($collection, $categoryQuery, $resolveInfo); + $depth = $this->depthCalculator->calculate($resolveInfo, $categoryQuery); $level = $this->levelCalculator->calculate($rootCategoryId); // If root category is being filter, we've to remove first slash @@ -124,24 +125,27 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato * * @param Collection $collection * @param FieldNode $fieldNode + * @param ResolveInfo $resolveInfo * @return void */ - private function joinAttributesRecursively(Collection $collection, FieldNode $fieldNode) : void - { + private function joinAttributesRecursively( + Collection $collection, + FieldNode $fieldNode, + ResolveInfo $resolveInfo + ): void { if (!isset($fieldNode->selectionSet->selections)) { return; } $subSelection = $fieldNode->selectionSet->selections; - $this->attributesJoiner->join($fieldNode, $collection); + $this->attributesJoiner->join($fieldNode, $collection, $resolveInfo); /** @var FieldNode $node */ foreach ($subSelection as $node) { - if ($node->kind === 'InlineFragment') { + if ($node->kind === NodeKind::INLINE_FRAGMENT || $node->kind === NodeKind::FRAGMENT_SPREAD) { continue; } - - $this->joinAttributesRecursively($collection, $node); + $this->joinAttributesRecursively($collection, $node, $resolveInfo); } } } diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index 5fec7bfd4fda7..03f9d7ad03f04 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -74,4 +74,14 @@ <preference type="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\Provider" for="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface"/> <preference type="Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search" for="Magento\CatalogGraphQl\Model\Resolver\Products\Query\ProductQueryInterface"/> + + <type name="\Magento\CatalogGraphQl\Model\Resolver\Product\BatchProductLinks"> + <arguments> + <argument name="linkTypes" xsi:type="array"> + <item name="related" xsi:type="string">related</item> + <item name="upsell" xsi:type="string">upsell</item> + <item name="crosssell" xsi:type="string">crosssell</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index c5fcac99767bd..189bfa61f2c42 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1595,6 +1595,7 @@ protected function _saveProducts() } $rowSku = $rowData[self::COL_SKU]; + $rowSkuNormalized = mb_strtolower($rowSku); if (null === $rowSku) { $this->getErrorAggregator()->addRowToSkip($rowNum); @@ -1604,9 +1605,9 @@ protected function _saveProducts() $storeId = !empty($rowData[self::COL_STORE]) ? $this->getStoreIdByCode($rowData[self::COL_STORE]) : Store::DEFAULT_STORE_ID; - $rowExistingImages = $existingImages[$storeId][$rowSku] ?? []; + $rowExistingImages = $existingImages[$storeId][$rowSkuNormalized] ?? []; $rowStoreMediaGalleryValues = $rowExistingImages; - $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSku] ?? []; + $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSkuNormalized] ?? []; if (self::SCOPE_STORE == $rowScope) { // set necessary data from SCOPE_DEFAULT row @@ -1762,10 +1763,11 @@ protected function _saveProducts() continue; } - if (isset($rowExistingImages[$uploadedFile])) { - $currentFileData = $rowExistingImages[$uploadedFile]; + $uploadedFileNormalized = ltrim($uploadedFile, '/\\'); + if (isset($rowExistingImages[$uploadedFileNormalized])) { + $currentFileData = $rowExistingImages[$uploadedFileNormalized]; $currentFileData['store_id'] = $storeId; - $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFile]); + $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFileNormalized]); if (array_key_exists($uploadedFile, $imageHiddenStates) && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] ) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php index a94a87a44b32a..d4694b72ba64f 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php @@ -384,7 +384,9 @@ public function getExistingImages(array $bunch) foreach ($this->connection->fetchAll($select) as $image) { $storeId = $image['store_id']; unset($image['store_id']); - $result[$storeId][$image['sku']][$image['value']] = $image; + $sku = mb_strtolower($image['sku']); + $value = ltrim($image['value'], '/\\'); + $result[$storeId][$sku][$value] = $image; } return $result; diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php index 332bb991bf29f..b2aaa054ebc34 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php @@ -176,15 +176,16 @@ public function getCurrencyRate() * * @param float|string $fromPrice * @param float|string $toPrice + * @param boolean $isLast * @return float|\Magento\Framework\Phrase */ - protected function _renderRangeLabel($fromPrice, $toPrice) + protected function _renderRangeLabel($fromPrice, $toPrice, $isLast = false) { $fromPrice = empty($fromPrice) ? 0 : $fromPrice * $this->getCurrencyRate(); $toPrice = empty($toPrice) ? $toPrice : $toPrice * $this->getCurrencyRate(); $formattedFromPrice = $this->priceCurrency->format($fromPrice); - if ($toPrice === '') { + if ($isLast) { return __('%1 and above', $formattedFromPrice); } elseif ($fromPrice == $toPrice && $this->dataProvider->getOnePriceIntervalValue()) { return $formattedFromPrice; @@ -215,12 +216,15 @@ protected function _getItemsData() $data = []; if (count($facets) > 1) { // two range minimum + $lastFacet = array_key_last($facets); foreach ($facets as $key => $aggregation) { $count = $aggregation['count']; if (strpos($key, '_') === false) { continue; } - $data[] = $this->prepareData($key, $count, $data); + + $isLast = $lastFacet === $key; + $data[] = $this->prepareData($key, $count, $isLast); } } @@ -264,18 +268,13 @@ protected function getFrom($from) * * @param string $key * @param int $count + * @param boolean $isLast * @return array */ - private function prepareData($key, $count) + private function prepareData($key, $count, $isLast = false) { - list($from, $to) = explode('_', $key); - if ($from == '*') { - $from = $this->getFrom($to); - } - if ($to == '*') { - $to = $this->getTo($to); - } - $label = $this->_renderRangeLabel($from, $to); + [$from, $to] = explode('_', $key); + $label = $this->_renderRangeLabel($from, $to, $isLast); $value = $from . '-' . $to . $this->dataProvider->getAdditionalRequestData(); $data = [ diff --git a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php index 257f8ba9bfe53..9e360481e8eb3 100644 --- a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php +++ b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php @@ -7,6 +7,7 @@ namespace Magento\CmsUrlRewrite\Plugin\Cms\Model\Store; +use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\PageRepositoryInterface; use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; use Magento\Framework\Api\SearchCriteriaBuilder; @@ -21,6 +22,8 @@ */ class View { + private const ALL_STORE_VIEWS = '0'; + /** * @var UrlPersistInterface */ @@ -89,9 +92,8 @@ private function generateCmsPagesUrls(int $storeId): array { $rewrites = []; $urls = []; - $searchCriteria = $this->searchCriteriaBuilder->create(); - $cmsPagesCollection = $this->pageRepository->getList($searchCriteria)->getItems(); - foreach ($cmsPagesCollection as $page) { + + foreach ($this->getCmsPageItems() as $page) { $page->setStoreId($storeId); $rewrites[] = $this->cmsPageUrlRewriteGenerator->generate($page); } @@ -99,4 +101,18 @@ private function generateCmsPagesUrls(int $storeId): array return $urls; } + + /** + * Return cms page items for all store view + * + * @return PageInterface[] + */ + private function getCmsPageItems(): array + { + $searchCriteria = $this->searchCriteriaBuilder->addFilter('store_id', self::ALL_STORE_VIEWS) + ->create(); + $list = $this->pageRepository->getList($searchCriteria); + + return $list->getItems(); + } } diff --git a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php index 2be340c8ccca4..b4c737f6600bf 100644 --- a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php +++ b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php @@ -52,7 +52,7 @@ public function getFrontendLabel(string $attributeCode): string { try { $attribute = $this->addressMetadata->getAttributeMetadata($attributeCode); - $frontendLabel = $attribute->getFrontendLabel(); + $frontendLabel = $attribute->getStoreLabel() ?: $attribute->getFrontendLabel(); } catch (NoSuchEntityException $e) { $frontendLabel = ''; } diff --git a/app/code/Magento/Customer/Model/CustomerRegistry.php b/app/code/Magento/Customer/Model/CustomerRegistry.php index d68904f6d1645..f2868132790cf 100644 --- a/app/code/Magento/Customer/Model/CustomerRegistry.php +++ b/app/code/Magento/Customer/Model/CustomerRegistry.php @@ -101,8 +101,10 @@ public function retrieve($customerId) public function retrieveByEmail($customerEmail, $websiteId = null) { if ($websiteId === null) { - $websiteId = $this->storeManager->getStore()->getWebsiteId(); + $websiteId = $this->storeManager->getStore()->getWebsiteId() + ?: $this->storeManager->getDefaultStoreView()->getWebsiteId(); } + $emailKey = $this->getEmailKey($customerEmail, $websiteId); if (isset($this->customerRegistryByEmail[$emailKey])) { return $this->customerRegistryByEmail[$emailKey]; diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php index 9eb9ffb806c9f..b877b2cca67a5 100644 --- a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php +++ b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php @@ -7,7 +7,10 @@ namespace Magento\Customer\Model\Plugin; use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; use Magento\Integration\Api\AuthorizationServiceInterface as AuthorizationService; +use Magento\Store\Model\StoreManagerInterface; /** * Plugin around \Magento\Framework\Authorization::isAllowed @@ -19,16 +22,41 @@ class CustomerAuthorization /** * @var UserContextInterface */ - protected $userContext; + private $userContext; + + /** + * @var CustomerFactory + */ + private $customerFactory; + + /** + * @var CustomerResource + */ + private $customerResource; + + /** + * @var StoreManagerInterface + */ + private $storeManager; /** * Inject dependencies. * * @param UserContextInterface $userContext + * @param CustomerFactory $customerFactory + * @param CustomerResource $customerResource + * @param StoreManagerInterface $storeManager */ - public function __construct(UserContextInterface $userContext) - { + public function __construct( + UserContextInterface $userContext, + CustomerFactory $customerFactory, + CustomerResource $customerResource, + StoreManagerInterface $storeManager + ) { $this->userContext = $userContext; + $this->customerFactory = $customerFactory; + $this->customerResource = $customerResource; + $this->storeManager = $storeManager; } /** @@ -53,9 +81,15 @@ public function aroundIsAllowed( && $this->userContext->getUserId() && $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER ) { - return true; - } else { - return $proceed($resource, $privilege); + $customer = $this->customerFactory->create(); + $this->customerResource->load($customer, $this->userContext->getUserId()); + $currentStoreId = $this->storeManager->getStore()->getId(); + $sharedStoreIds = $customer->getSharedStoreIds(); + if (in_array($currentStoreId, $sharedStoreIds)) { + return true; + } } + + return $proceed($resource, $privilege); } } diff --git a/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php new file mode 100644 index 0000000000000..ef3e86788c43f --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Api; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Interface for customer data validator + */ +interface ValidateCustomerDataInterface +{ + /** + * Validate customer data + * + * @param array $customerData + * @throws GraphQlInputException + */ + public function execute(array $customerData): void; +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php index 3861ce324ea7d..95d68d69d71e7 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php @@ -7,6 +7,9 @@ namespace Magento\CustomerGraphQl\Model\Customer; +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\Validator\EmailAddress as EmailAddressValidator; @@ -27,53 +30,41 @@ class ValidateCustomerData */ private $emailAddressValidator; + /** + * @var ValidateCustomerDataInterface[] + */ + private $validators = []; + /** * ValidateCustomerData constructor. * * @param GetAllowedCustomerAttributes $getAllowedCustomerAttributes * @param EmailAddressValidator $emailAddressValidator + * @param array $validators */ public function __construct( GetAllowedCustomerAttributes $getAllowedCustomerAttributes, - EmailAddressValidator $emailAddressValidator + EmailAddressValidator $emailAddressValidator, + $validators = [] ) { $this->getAllowedCustomerAttributes = $getAllowedCustomerAttributes; $this->emailAddressValidator = $emailAddressValidator; + $this->validators = $validators; } /** * Validate customer data * * @param array $customerData - * - * @return void - * * @throws GraphQlInputException + * @throws LocalizedException + * @throws NoSuchEntityException */ - public function execute(array $customerData): void + public function execute(array $customerData) { - $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData)); - $errorInput = []; - - foreach ($attributes as $attributeInfo) { - if ($attributeInfo->getIsRequired() - && (!isset($customerData[$attributeInfo->getAttributeCode()]) - || $customerData[$attributeInfo->getAttributeCode()] == '') - ) { - $errorInput[] = $attributeInfo->getDefaultFrontendLabel(); - } - } - - if ($errorInput) { - throw new GraphQlInputException( - __('Required parameters are missing: %1', [implode(', ', $errorInput)]) - ); - } - - if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) { - throw new GraphQlInputException( - __('"%1" is not a valid email address.', $customerData['email']) - ); + /** @var ValidateCustomerDataInterface $validator */ + foreach ($this->validators as $validator) { + $validator->execute($customerData); } } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php new file mode 100644 index 0000000000000..87f8831550f04 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\Validator\EmailAddress as EmailAddressValidator; + +/** + * Validates an email + */ +class ValidateEmail implements ValidateCustomerDataInterface +{ + /** + * @var EmailAddressValidator + */ + private $emailAddressValidator; + + /** + * ValidateEmail constructor. + * + * @param EmailAddressValidator $emailAddressValidator + */ + public function __construct(EmailAddressValidator $emailAddressValidator) + { + $this->emailAddressValidator = $emailAddressValidator; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) { + throw new GraphQlInputException( + __('"%1" is not a valid email address.', $customerData['email']) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php new file mode 100644 index 0000000000000..463372a63d8d5 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Model\Data\AttributeMetadata; +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Validates gender value + */ +class ValidateGender implements ValidateCustomerDataInterface +{ + /** + * @var CustomerMetadataInterface + */ + private $customerMetadata; + + /** + * ValidateGender constructor. + * + * @param CustomerMetadataInterface $customerMetadata + */ + public function __construct(CustomerMetadataInterface $customerMetadata) + { + $this->customerMetadata = $customerMetadata; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + if (isset($customerData['gender']) && $customerData['gender']) { + /** @var AttributeMetadata $genderData */ + $options = $this->customerMetadata->getAttributeMetadata('gender')->getOptions(); + + $isValid = false; + foreach ($options as $optionData) { + if ($optionData->getValue() && $optionData->getValue() == $customerData['gender']) { + $isValid = true; + } + } + + if (!$isValid) { + throw new GraphQlInputException( + __('"%1" is not a valid gender value.', $customerData['gender']) + ); + } + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php new file mode 100644 index 0000000000000..fdf4fa1c824f2 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\CustomerGraphQl\Model\Customer\GetAllowedCustomerAttributes; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Validates required attributes + */ +class ValidateRequiredArguments implements ValidateCustomerDataInterface +{ + /** + * Get allowed/required customer attributes + * + * @var GetAllowedCustomerAttributes + */ + private $getAllowedCustomerAttributes; + + /** + * ValidateRequiredArguments constructor. + * + * @param GetAllowedCustomerAttributes $getAllowedCustomerAttributes + */ + public function __construct(GetAllowedCustomerAttributes $getAllowedCustomerAttributes) + { + $this->getAllowedCustomerAttributes = $getAllowedCustomerAttributes; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData)); + $errorInput = []; + + foreach ($attributes as $attributeInfo) { + if ($attributeInfo->getIsRequired() + && (!isset($customerData[$attributeInfo->getAttributeCode()]) + || $customerData[$attributeInfo->getAttributeCode()] == '') + ) { + $errorInput[] = $attributeInfo->getDefaultFrontendLabel(); + } + } + + if ($errorInput) { + throw new GraphQlInputException( + __('Required parameters are missing: %1', [implode(', ', $errorInput)]) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml index 1ba0e457430e0..3ed77a2ad563c 100644 --- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml @@ -29,4 +29,14 @@ </argument> </arguments> </type> + <!-- Validate input customer data --> + <type name="Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData"> + <arguments> + <argument name="validators" xsi:type="array"> + <item name="validateRequiredArguments" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateRequiredArguments</item> + <item name="validateEmail" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateEmail</item> + <item name="validateGender" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateGender</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Developer/Console/Command/patch_template.php.dist b/app/code/Magento/Developer/Console/Command/patch_template.php.dist index f4fc25abcb29a..8e14b24bdc933 100644 --- a/app/code/Magento/Developer/Console/Command/patch_template.php.dist +++ b/app/code/Magento/Developer/Console/Command/patch_template.php.dist @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace %moduleName%\Setup\Patch\%patchType%; @@ -36,7 +37,7 @@ class %class% implements %implementsInterfaces% } %revertFunction% /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { @@ -44,12 +45,10 @@ class %class% implements %implementsInterfaces% } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { - return [ - - ]; + return []; } } diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php index 1f6e05c9e02fc..8576d8df0cc95 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php @@ -19,7 +19,7 @@ class Converter implements ConverterInterface */ private const ES_DATA_TYPE_TEXT = 'text'; private const ES_DATA_TYPE_KEYWORD = 'keyword'; - private const ES_DATA_TYPE_FLOAT = 'float'; + private const ES_DATA_TYPE_DOUBLE = 'double'; private const ES_DATA_TYPE_INT = 'integer'; private const ES_DATA_TYPE_DATE = 'date'; /**#@-*/ @@ -32,7 +32,7 @@ class Converter implements ConverterInterface private $mapping = [ self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_TEXT, self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_KEYWORD, - self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT, + self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE, self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, ]; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php index bd9a380230652..8d8787a5eff72 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php @@ -276,7 +276,7 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php index 88dab83698794..2067dcdc7fe9f 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php @@ -16,7 +16,7 @@ class Converter implements ConverterInterface * Text flags for Elasticsearch field types */ private const ES_DATA_TYPE_STRING = 'string'; - private const ES_DATA_TYPE_FLOAT = 'float'; + private const ES_DATA_TYPE_DOUBLE = 'double'; private const ES_DATA_TYPE_INT = 'integer'; private const ES_DATA_TYPE_DATE = 'date'; /**#@-*/ @@ -29,7 +29,7 @@ class Converter implements ConverterInterface private $mapping = [ self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_STRING, self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_STRING, - self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT, + self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE, self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, ]; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php index 1e106023ea00d..548a57e55f3e2 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php @@ -35,7 +35,7 @@ public function __construct(Repository $algorithmRepository, EntityStorageFactor } /** - * {@inheritdoc} + * @inheritdoc */ public function build( RequestBucketInterface $bucket, @@ -46,9 +46,7 @@ public function build( /** @var DynamicBucket $bucket */ $algorithm = $this->algorithmRepository->get($bucket->getMethod(), ['dataProvider' => $dataProvider]); $data = $algorithm->getItems($bucket, $dimensions, $this->getEntityStorage($queryResult)); - $resultData = $this->prepareData($data); - - return $resultData; + return $this->prepareData($data); } /** @@ -77,12 +75,9 @@ private function prepareData($data) { $resultData = []; foreach ($data as $value) { - $from = is_numeric($value['from']) ? $value['from'] : '*'; - $to = is_numeric($value['to']) ? $value['to'] : '*'; - unset($value['from'], $value['to']); - - $rangeName = "{$from}_{$to}"; - $resultData[$rangeName] = array_merge(['value' => $rangeName], $value); + $rangeName = "{$value['from']}_{$value['to']}"; + $value['value'] = $rangeName; + $resultData[$rangeName] = $value; } return $resultData; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php index 496a77e4c5ac3..7bc64b59ffe78 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php @@ -235,11 +235,9 @@ public function prepareData($range, array $dbRanges) { $data = []; if (!empty($dbRanges)) { - $lastIndex = array_keys($dbRanges); - $lastIndex = $lastIndex[count($lastIndex) - 1]; foreach ($dbRanges as $index => $count) { - $fromPrice = $index == 1 ? '' : ($index - 1) * $range; - $toPrice = $index == $lastIndex ? '' : $index * $range; + $fromPrice = $index == 1 ? 0 : ($index - 1) * $range; + $toPrice = $index * $range; $data[] = [ 'from' => $fromPrice, 'to' => $toPrice, diff --git a/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php new file mode 100644 index 0000000000000..7cd72c322d647 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Elasticsearch\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; +use Magento\Framework\Setup\Patch\PatchInterface; + +/** + * Invalidate fulltext index + */ +class InvalidateIndex implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var IndexerRegistry + */ + private $indexerRegistry; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param IndexerRegistry $indexerRegistry + */ + public function __construct(ModuleDataSetupInterface $moduleDataSetup, IndexerRegistry $indexerRegistry) + { + $this->moduleDataSetup = $moduleDataSetup; + $this->indexerRegistry = $indexerRegistry; + } + + /** + * @inheritDoc + */ + public function apply(): PatchInterface + { + $this->indexerRegistry->get(FulltextIndexer::INDEXER_ID)->invalidate(); + return $this; + } + + /** + * @inheritDoc + */ + public static function getDependencies(): array + { + return []; + } + + /** + * @inheritDoc + */ + public function getAliases(): array + { + return []; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php index 49a894f1295c7..575a64dc43abd 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php @@ -329,7 +329,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -400,7 +400,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php index 87f072836544e..a9bcd1a20a1b2 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php @@ -246,7 +246,7 @@ function ($type) use ($complexType) { if ($type === 'string') { return 'string'; } elseif ($type === 'float') { - return 'float'; + return 'double'; } elseif ($type === 'integer') { return 'integer'; } else { @@ -281,7 +281,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ] @@ -300,7 +300,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ], @@ -319,7 +319,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ] diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php index 75b79bc43e805..718adf255254f 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php @@ -56,7 +56,7 @@ public function convertProvider() { return [ ['string', 'string'], - ['float', 'float'], + ['float', 'double'], ['integer', 'integer'], ]; } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php index c5b9089acd91c..0595b667f4ee8 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php @@ -390,13 +390,13 @@ public function testPrepareData() { $expectedResult = [ [ - 'from' => '', + 'from' => 0, 'to' => 10, 'count' => 1, ], [ 'from' => 10, - 'to' => '', + 'to' => 20, 'count' => 1, ], ]; diff --git a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php index 2c1c283c5b24d..0571b075aff28 100644 --- a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php @@ -281,7 +281,7 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php index aa0b49400c517..2a7fa2ce8114a 100644 --- a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php @@ -439,7 +439,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -509,7 +509,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php index feacca8d62804..4b318f987abfe 100644 --- a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php @@ -289,7 +289,7 @@ public function addFieldsMapping(array $fields, string $index, string $entityTyp 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php index 593bbd7792f46..091387f844d55 100644 --- a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php @@ -438,7 +438,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -509,7 +509,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php index c317221fb6ef7..2ce51c8bbf19d 100644 --- a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php @@ -66,7 +66,7 @@ public function resolve( array $args = null ) { if (!isset($value['model'])) { - throw new GraphQlInputException(__('"model" value should be specified')); + throw new GraphQlInputException(__('"model" value must be specified')); } $cart = $value['model']; diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php new file mode 100644 index 0000000000000..a9a8e682612cc --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GiftMessageGraphQl\Model\Resolver\Cart\Item; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GiftMessage\Api\ItemRepositoryInterface; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; + +/** + * Class provides ability to get GiftMessage for cart item + */ +class GiftMessage implements ResolverInterface +{ + /** + * @var ItemRepositoryInterface + */ + private $itemRepository; + + /** + * @var GiftMessageHelper + */ + private $giftMessageHelper; + + /** + * @param ItemRepositoryInterface $itemRepository + * @param GiftMessageHelper $giftMessageHelper + */ + public function __construct( + ItemRepositoryInterface $itemRepository, + GiftMessageHelper $giftMessageHelper + ) { + $this->itemRepository = $itemRepository; + $this->giftMessageHelper = $giftMessageHelper; + } + + /** + * Return information about Gift message for item cart + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('"model" value must be specified')); + } + + $quoteItem = $value['model']; + + if (!$this->giftMessageHelper->isMessagesAllowed('items', $quoteItem)) { + return null; + } + + if (!$this->giftMessageHelper->isMessagesAllowed('item', $quoteItem)) { + return null; + } + + try { + $giftItemMessage = $this->itemRepository->get($quoteItem->getQuoteId(), $quoteItem->getItemId()); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__('Can\'t load cart item')); + } + + if (!isset($giftItemMessage)) { + return null; + } + + return [ + 'to' => $giftItemMessage->getRecipient() ?? '', + 'from' => $giftItemMessage->getSender() ?? '', + 'message'=> $giftItemMessage->getMessage() ?? '' + ]; + } +} diff --git a/app/code/Magento/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md index fa2e02116b66c..d73a058e0db9c 100644 --- a/app/code/Magento/GiftMessageGraphQl/README.md +++ b/app/code/Magento/GiftMessageGraphQl/README.md @@ -1,3 +1,3 @@ # GiftMessageGraphQl -**GiftMessageGraphQl** provides information about gift messages for cart, cart items, order and order items. +**GiftMessageGraphQl** provides information about gift messages for carts, cart items, orders and order items. diff --git a/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..bce5b7063e6b9 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="allow_order" xsi:type="string">sales/gift_options/allow_order</item> + <item name="allow_items" xsi:type="string">sales/gift_options/allow_items</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls index f14c812a9a5f3..ad18054abca13 100644 --- a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls +++ b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls @@ -1,20 +1,47 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. +type StoreConfig { + allow_order : String @doc(description: "The value of the Allow Gift Messages on Order Level option") + allow_items : String @doc(description: "The value of the Allow Gift Messages for Order Items option") +} + type Cart { gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\GiftMessage") @doc(description: "The entered gift message for the cart") } -type SalesItemInterface { - gift_message: GiftMessage @doc(description: "The entered gift message for the order item") +type SimpleCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") } -type CustomerOrder { - gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order") +type ConfigurableCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") +} + +type BundleCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") +} + +type GiftMessage @doc(description: "Contains the text of a gift message, its sender, and recipient") { + to: String! @doc(description: "Recipient name") + from: String! @doc(description: "Sender name") + message: String! @doc(description: "Gift message text") +} + +input CartItemUpdateInput { + gift_message: GiftMessageInput @doc(description: "Gift message details for the cart item") } -type GiftMessage { - to: String! @doc(description: "Recepient name") +input GiftMessageInput @doc(description: "Contains the text of a gift message, its sender, and recipient") { + to: String! @doc(description: "Recipient name") from: String! @doc(description: "Sender name") message: String! @doc(description: "Gift message text") } + +type SalesItemInterface { + gift_message: GiftMessage @doc(description: "The entered gift message for the order item") +} + +type CustomerOrder { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order") +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php new file mode 100644 index 0000000000000..ba2e995d4f704 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Credentials header if CORS is enabled + */ +class CorsAllowCredentialsHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Get value for header + * + * @return string + */ + public function getValue(): string + { + return "1"; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->corsConfiguration->isCredentialsAllowed(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php new file mode 100644 index 0000000000000..68760de543daa --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Headers header if CORS is enabled + */ +class CorsAllowHeadersHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedHeaders(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php new file mode 100644 index 0000000000000..233839b9deb74 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Methods header if CORS is enabled + */ +class CorsAllowMethodsHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedMethods(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php new file mode 100644 index 0000000000000..21850f18db1f2 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Origin header if CORS is enabled + */ +class CorsAllowOriginHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedOrigins(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php new file mode 100644 index 0000000000000..e30209ae25e68 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Max-Age header if CORS is enabled + */ +class CorsMaxAgeHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return (string) $this->corsConfiguration->getMaxAge(); + } +} diff --git a/app/code/Magento/GraphQl/Model/Cors/Configuration.php b/app/code/Magento/GraphQl/Model/Cors/Configuration.php new file mode 100644 index 0000000000000..dd5a0b426e22d --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Cors/Configuration.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Cors; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Configuration provider for GraphQL CORS settings + */ +class Configuration implements ConfigurationInterface +{ + public const XML_PATH_CORS_HEADERS_ENABLED = 'graphql/cors/enabled'; + public const XML_PATH_CORS_ALLOWED_ORIGINS = 'graphql/cors/allowed_origins'; + public const XML_PATH_CORS_ALLOWED_HEADERS = 'graphql/cors/allowed_headers'; + public const XML_PATH_CORS_ALLOWED_METHODS = 'graphql/cors/allowed_methods'; + public const XML_PATH_CORS_MAX_AGE = 'graphql/cors/max_age'; + public const XML_PATH_CORS_ALLOW_CREDENTIALS = 'graphql/cors/allow_credentials'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Are CORS headers enabled + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_HEADERS_ENABLED); + } + + /** + * Get allowed origins or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedOrigins(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_ORIGINS); + } + + /** + * Get allowed headers or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedHeaders(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_HEADERS); + } + + /** + * Get allowed methods or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedMethods(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_METHODS); + } + + /** + * Get max age header value + * + * @return int + */ + public function getMaxAge(): int + { + return (int) $this->scopeConfig->getValue(self::XML_PATH_CORS_MAX_AGE); + } + + /** + * Are credentials allowed + * + * @return bool + */ + public function isCredentialsAllowed(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_ALLOW_CREDENTIALS); + } +} diff --git a/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php new file mode 100644 index 0000000000000..b40b64f48e51f --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Cors; + +/** + * Interface for configuration provider for GraphQL CORS settings + */ +interface ConfigurationInterface +{ + /** + * Are CORS headers enabled + * + * @return bool + */ + public function isEnabled(): bool; + + /** + * Get allowed origins or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedOrigins(): ?string; + + /** + * Get allowed headers or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedHeaders(): ?string; + + /** + * Get allowed methods or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedMethods(): ?string; + + /** + * Get max age header value + * + * @return int + */ + public function getMaxAge(): int; + + /** + * Are credentials allowed + * + * @return bool + */ + public function isCredentialsAllowed() : bool; +} diff --git a/app/code/Magento/GraphQl/etc/adminhtml/system.xml b/app/code/Magento/GraphQl/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..ddee7596eca3e --- /dev/null +++ b/app/code/Magento/GraphQl/etc/adminhtml/system.xml @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="graphql" translate="label" type="text" sortOrder="300" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>GraphQL</label> + <tab>service</tab> + <resource>Magento_Integration::config_oauth</resource> + <group id="cors" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="1"> + <label>CORS Settings</label> + <field id="enabled" translate="label" type="select" sortOrder="1" showInDefault="1" canRestore="1"> + <label>CORS Headers Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + + <field id="allowed_origins" translate="label" type="text" sortOrder="10" showInDefault="1" canRestore="1"> + <label>Allowed origins</label> + <comment>The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin. Fill this field with one or more origins (comma separated) or use '*' to allow access from all origins.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allowed_methods" translate="label" type="text" sortOrder="20" showInDefault="1" canRestore="1"> + <label>Allowed methods</label> + <comment>The Access-Control-Allow-Methods response header specifies the method or methods allowed when accessing the resource in response to a preflight request. Use comma separated methods (e.g. GET,POST)</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allowed_headers" translate="label" type="text" sortOrder="30" showInDefault="1" canRestore="1"> + <label>Allowed headers</label> + <comment>The Access-Control-Allow-Headers response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request. Use comma separated headers.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="max_age" translate="label" type="text" sortOrder="40" showInDefault="1" canRestore="1"> + <label>Max Age</label> + <validate>validate-digits</validate> + <comment>The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allow_credentials" translate="label" type="select" sortOrder="50" showInDefault="1" canRestore="1"> + <label>Credentials Allowed</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to frontend code when the request's credentials mode is include.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/GraphQl/etc/config.xml b/app/code/Magento/GraphQl/etc/config.xml new file mode 100644 index 0000000000000..39caacbec42d2 --- /dev/null +++ b/app/code/Magento/GraphQl/etc/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <graphql> + <cors> + <enabled>0</enabled> + <allowed_origins></allowed_origins> + <allowed_methods></allowed_methods> + <allowed_headers></allowed_headers> + <max_age>86400</max_age> + <allow_credentials>0</allow_credentials> + </cors> + </graphql> + </default> +</config> diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml index b356f33c4f4bf..fca6c425e2507 100644 --- a/app/code/Magento/GraphQl/etc/di.xml +++ b/app/code/Magento/GraphQl/etc/di.xml @@ -98,4 +98,31 @@ <argument name="queryComplexity" xsi:type="number">300</argument> </arguments> </type> + + <preference for="Magento\GraphQl\Model\Cors\ConfigurationInterface" type="Magento\GraphQl\Model\Cors\Configuration" /> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Max-Age</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Credentials</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Headers</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Methods</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Origin</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/GraphQl/etc/graphql/di.xml b/app/code/Magento/GraphQl/etc/graphql/di.xml index 77fce336374dd..23d49124d1a02 100644 --- a/app/code/Magento/GraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GraphQl/etc/graphql/di.xml @@ -30,4 +30,15 @@ </argument> </arguments> </type> + <type name="Magento\Framework\App\Response\HeaderManager"> + <arguments> + <argument name="headerProviderList" xsi:type="array"> + <item name="CorsAllowOrigins" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider</item> + <item name="CorsAllowHeaders" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider</item> + <item name="CorsAllowMethods" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider</item> + <item name="CorsAllowCredentials" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider</item> + <item name="CorsMaxAge" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php index 92cfb375fea41..29fa2bffabb3b 100644 --- a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php +++ b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php @@ -10,7 +10,7 @@ use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class GroupedProductLinksTypeResolver implements TypeResolverInterface { @@ -20,14 +20,14 @@ class GroupedProductLinksTypeResolver implements TypeResolverInterface private $linkTypes = ['associated']; /** - * {@inheritdoc} + * @inheritdoc */ - public function resolveType(array $data) : string + public function resolveType(array $data): string { if (isset($data['link_type'])) { $linkType = $data['link_type']; if (in_array($linkType, $this->linkTypes)) { - return 'GroupedProductLinks'; + return 'ProductLinks'; } } return ''; diff --git a/app/code/Magento/GroupedProductGraphQl/etc/di.xml b/app/code/Magento/GroupedProductGraphQl/etc/di.xml index 35b63370baf2f..717bc14826f70 100644 --- a/app/code/Magento/GroupedProductGraphQl/etc/di.xml +++ b/app/code/Magento/GroupedProductGraphQl/etc/di.xml @@ -13,4 +13,11 @@ </argument> </arguments> </type> + <type name="\Magento\CatalogGraphQl\Model\Resolver\Product\BatchProductLinks"> + <arguments> + <argument name="linkTypes" xsi:type="array"> + <item name="associated" xsi:type="string">associated</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php index 09b17371ae4e8..f5b62df9aea2c 100644 --- a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php +++ b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php @@ -54,6 +54,19 @@ public function destruct() { if (is_object($this->_fileHandler)) { $this->_fileHandler->close(); + $this->resolveDestination(); + } + } + + /** + * Remove temporary destination + * + * @return void + */ + private function resolveDestination(): void + { + // only temporary file located directly in var folder + if (strpos($this->_destination, '/') === false) { $this->_directoryHandle->delete($this->_destination); } } diff --git a/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php new file mode 100644 index 0000000000000..1565d455cc43f --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Observer; + +use Magento\Catalog\Model\Category as CatalogCategory; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the catalog_category_delete_after event and deletes relation between category content and media asset. + */ +class CategoryDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'catalog_category'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $category = $observer->getEvent()->getData('category'); + $contentAssetLinks = []; + + if ($category instanceof CatalogCategory) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $category->getEntityId(), + ] + ); + $content = implode(PHP_EOL, $this->getContent->execute($contentIdentity)); + $assets = $this->extractAssetsFromContent->execute($content); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php new file mode 100644 index 0000000000000..421bb5a33fa1d --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Observer; + +use Magento\Catalog\Model\Product as CatalogProduct; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the catalog_product_delete_before event and deletes relation between category content and media asset. + */ +class ProductDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'catalog_product'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted product and remove relation betwen product and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $product = $observer->getEvent()->getData('product'); + $contentAssetLinks = []; + + if ($product instanceof CatalogProduct) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $product->getEntityId(), + ] + ); + $productContent = implode(PHP_EOL, $this->getContent->execute($contentIdentity)); + $assets = $this->extractAssetsFromContent->execute($productContent); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCatalog/etc/di.xml b/app/code/Magento/MediaContentCatalog/etc/di.xml index a2d300a2bb208..6b0ee83b30788 100644 --- a/app/code/Magento/MediaContentCatalog/etc/di.xml +++ b/app/code/Magento/MediaContentCatalog/etc/di.xml @@ -14,6 +14,22 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentCatalog\Observer\ProductDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="description" xsi:type="string">description</item> + <item name="short_description" xsi:type="string">short_description</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentCatalog\Observer\CategoryDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="image" xsi:type="string">image</item> + <item name="description" xsi:type="string">description</item> + </argument> + </arguments> + </type> <type name="Magento\MediaContentCatalog\Observer\Category"> <arguments> <argument name="fields" xsi:type="array"> diff --git a/app/code/Magento/MediaContentCatalog/etc/events.xml b/app/code/Magento/MediaContentCatalog/etc/events.xml index f68d66eb3cc40..8ec7a30b961ba 100644 --- a/app/code/Magento/MediaContentCatalog/etc/events.xml +++ b/app/code/Magento/MediaContentCatalog/etc/events.xml @@ -9,6 +9,12 @@ <event name="catalog_category_save_after"> <observer name="media_content_catalog_category_save_after" instance="Magento\MediaContentCatalog\Observer\Category" /> </event> + <event name="catalog_product_delete_before"> + <observer name="media_content_catalog_product_delete_before" instance="Magento\MediaContentCatalog\Observer\ProductDelete" /> + </event> + <event name="catalog_category_delete_before"> + <observer name="media_content_catalog_category_delete_before" instance="Magento\MediaContentCatalog\Observer\CategoryDelete" /> + </event> <event name="catalog_product_save_after"> <observer name="media_content_catalog_product_save_after" instance="Magento\MediaContentCatalog\Observer\Product" /> </event> diff --git a/app/code/Magento/MediaContentCms/Observer/BlockDelete.php b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php new file mode 100644 index 0000000000000..582f0a9ec6701 --- /dev/null +++ b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Observer; + +use Magento\Cms\Model\Block as CmsBlock; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the adminhtml_cmspage_on_delete event and deletes relation between page content and media asset. + */ +class BlockDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'cms_block'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $block = $observer->getEvent()->getData('object'); + $contentAssetLinks = []; + + if ($block instanceof CmsBlock) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $block->getId(), + ] + ); + $assets = $this->extractAssetsFromContent->execute((string) $block->getData($field)); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCms/Observer/PageDelete.php b/app/code/Magento/MediaContentCms/Observer/PageDelete.php new file mode 100644 index 0000000000000..96d2bf89873bd --- /dev/null +++ b/app/code/Magento/MediaContentCms/Observer/PageDelete.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Observer; + +use Magento\Cms\Model\Page as CmsPage; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the cms_page_delete_before event and deletes relation between page content and media asset. + */ +class PageDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'cms_page'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param arry $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $page = $observer->getEvent()->getData('object'); + $contentAssetLinks = []; + + if ($page instanceof CmsPage) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $page->getId(), + ] + ); + + $assets = $this->extractAssetsFromContent->execute((string) $page->getData($field)); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCms/etc/di.xml b/app/code/Magento/MediaContentCms/etc/di.xml index f980936465faf..6f196889540af 100644 --- a/app/code/Magento/MediaContentCms/etc/di.xml +++ b/app/code/Magento/MediaContentCms/etc/di.xml @@ -20,4 +20,18 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentCms\Observer\PageDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentCms\Observer\BlockDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaContentCms/etc/events.xml b/app/code/Magento/MediaContentCms/etc/events.xml index 7e9abe3bf19c4..94f963f40be15 100644 --- a/app/code/Magento/MediaContentCms/etc/events.xml +++ b/app/code/Magento/MediaContentCms/etc/events.xml @@ -6,8 +6,14 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="cms_page_delete_before"> + <observer name="media_content_cms_page_delete_before" instance="Magento\MediaContentCms\Observer\PageDelete" /> + </event> <event name="cms_page_save_after"> <observer name="media_content_cms_page_save_after" instance="Magento\MediaContentCms\Observer\Page" /> + </event> + <event name="cms_block_delete_before"> + <observer name="media_content_cms_block_delete_before" instance="Magento\MediaContentCms\Observer\BlockDelete" /> </event> <event name="cms_block_save_after"> <observer name="media_content_cms_block_save_after" instance="Magento\MediaContentCms\Observer\Block" /> diff --git a/app/code/Magento/Payment/Block/Transparent/Redirect.php b/app/code/Magento/Payment/Block/Transparent/Redirect.php index 1be6dec4cc1d8..97a09df38d120 100644 --- a/app/code/Magento/Payment/Block/Transparent/Redirect.php +++ b/app/code/Magento/Payment/Block/Transparent/Redirect.php @@ -53,10 +53,21 @@ public function getRedirectUrl(): string /** * Returns params to be redirected. * + * Encodes invalid UTF-8 values to UTF-8 to prevent character escape error. + * Some payment methods like PayPal, send data in merchant defined language encoding + * which can be different from the system character encoding (UTF-8). + * * @return array */ public function getPostParams(): array { - return (array)$this->_request->getPostValue(); + $params = []; + foreach ($this->_request->getPostValue() as $name => $value) { + if (!empty($value) && mb_detect_encoding($value, 'UTF-8', true) === false) { + $value = utf8_encode($value); + } + $params[$name] = $value; + } + return $params; } } diff --git a/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php new file mode 100644 index 0000000000000..1cd1230a14634 --- /dev/null +++ b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Payment\Test\Unit\Block\Transparent; + +use Magento\Payment\Block\Transparent\Redirect; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class RedirectTest extends TestCase +{ + /** + * @var \Magento\Framework\View\Element\Context|MockObject + */ + private $context; + /** + * @var \Magento\Framework\UrlInterface|MockObject + */ + private $url; + /** + * @var Redirect + */ + private $model; + /** + * @var \Magento\Framework\App\RequestInterface|MockObject + */ + private $request; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); + $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->context->method('getRequest') + ->willReturn($this->request); + $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); + $this->model = new Redirect( + $this->context, + $this->url + ); + } + + /** + * @param array $postData + * @param array $expected + * @dataProvider getPostParamsDataProvider + */ + public function testGetPostParams(array $postData, array $expected): void + { + $this->request->method('getPostValue') + ->willReturn($postData); + $this->assertEquals($expected, $this->model->getPostParams()); + } + + /** + * @return array + */ + public function getPostParamsDataProvider(): array + { + return [ + [ + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ], + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ] + ], + [ + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => mb_convert_encoding('Ãtienne', 'ISO-8859-1'), + 'BILLTOFIRSTNAME' => mb_convert_encoding('Ãillin', 'ISO-8859-1'), + ], + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ] + ] + ]; + } +} diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote.php b/app/code/Magento/Quote/Model/ResourceModel/Quote.php index 48945dacd1738..749e9944a6ad3 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote.php @@ -230,7 +230,8 @@ public function subtractProductFromQuotes($product) 'items_qty' => new \Zend_Db_Expr( $connection->quoteIdentifier('q.items_qty') . ' - ' . $connection->quoteIdentifier('qi.qty') ), - 'items_count' => new \Zend_Db_Expr($ifSql) + 'items_count' => new \Zend_Db_Expr($ifSql), + 'updated_at' => 'q.updated_at', ] )->join( ['qi' => $this->getTable('quote_item')], @@ -277,21 +278,27 @@ public function markQuotesRecollect($productIds) { $tableQuote = $this->getTable('quote'); $tableItem = $this->getTable('quote_item'); - $subSelect = $this->getConnection()->select()->from( - $tableItem, - ['entity_id' => 'quote_id'] - )->where( - 'product_id IN ( ? )', - $productIds - )->group( - 'quote_id' - ); - - $select = $this->getConnection()->select()->join( - ['t2' => $subSelect], - 't1.entity_id = t2.entity_id', - ['trigger_recollect' => new \Zend_Db_Expr('1')] - ); + $subSelect = $this->getConnection() + ->select() + ->from( + $tableItem, + ['entity_id' => 'quote_id'] + )->where( + 'product_id IN ( ? )', + $productIds + )->group( + 'quote_id' + ); + $select = $this->getConnection() + ->select() + ->join( + ['t2' => $subSelect], + 't1.entity_id = t2.entity_id', + [ + 'trigger_recollect' => new \Zend_Db_Expr('1'), + 'updated_at' => 't1.updated_at', + ] + ); $updateQuery = $select->crossUpdateFromSelect(['t1' => $tableQuote]); $this->getConnection()->query($updateQuery); diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index f73daa715c1df..e959c19a7cbe4 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -51,7 +51,10 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s $shippingAddressInput = current($shippingAddressesInput) ?? []; $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; - if (!$customerAddressId && !isset($shippingAddressInput['address']['save_in_address_book'])) { + if (!$customerAddressId + && isset($shippingAddressInput['address']) + && !isset($shippingAddressInput['address']['save_in_address_book']) + ) { $shippingAddressInput['address']['save_in_address_book'] = true; } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php index b2526bdc04e98..654a4bb558632 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php @@ -42,12 +42,12 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s } $shippingMethodInput = current($shippingMethodsInput); - if (!isset($shippingMethodInput['carrier_code']) || empty($shippingMethodInput['carrier_code'])) { + if (empty($shippingMethodInput['carrier_code'])) { throw new GraphQlInputException(__('Required parameter "carrier_code" is missing.')); } $carrierCode = $shippingMethodInput['carrier_code']; - if (!isset($shippingMethodInput['method_code']) || empty($shippingMethodInput['method_code'])) { + if (empty($shippingMethodInput['method_code'])) { throw new GraphQlInputException(__('Required parameter "method_code" is missing.')); } $methodCode = $shippingMethodInput['method_code']; diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php new file mode 100644 index 0000000000000..c2e94b215956e --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php @@ -0,0 +1,157 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\GiftMessage\Api\Data\MessageInterface; +use Magento\GiftMessage\Api\Data\MessageInterfaceFactory; +use Magento\GiftMessage\Api\ItemRepositoryInterface; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; +use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem; + +/** + * Class contain update cart items methods + */ +class UpdateCartItems +{ + /** + * @var CartItemRepositoryInterface + */ + private $cartItemRepository; + + /** + * @var UpdateCartItem + */ + private $updateCartItem; + + /** + * @var ItemRepositoryInterface + */ + private $itemRepository; + + /** + * @var GiftMessageHelper + */ + private $giftMessageHelper; + + /** + * @var MessageInterfaceFactory + */ + private $giftMessageFactory; + + /** + * @param CartItemRepositoryInterface $cartItemRepository + * @param UpdateCartItem $updateCartItem + * @param ItemRepositoryInterface $itemRepository + * @param GiftMessageHelper $giftMessageHelper + * @param MessageInterfaceFactory $giftMessageFactory + */ + public function __construct( + CartItemRepositoryInterface $cartItemRepository, + UpdateCartItem $updateCartItem, + ItemRepositoryInterface $itemRepository, + GiftMessageHelper $giftMessageHelper, + MessageInterfaceFactory $giftMessageFactory + ) { + $this->cartItemRepository = $cartItemRepository; + $this->updateCartItem = $updateCartItem; + $this->itemRepository = $itemRepository; + $this->giftMessageHelper = $giftMessageHelper; + $this->giftMessageFactory = $giftMessageFactory; + } + + /** + * Process cart items + * + * @param Quote $cart + * @param array $items + * + * @throws GraphQlInputException + * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function processCartItems(Quote $cart, array $items): void + { + foreach ($items as $item) { + if (empty($item['cart_item_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.')); + } + + $itemId = (int)$item['cart_item_id']; + $customizableOptions = $item['customizable_options'] ?? []; + $cartItem = $cart->getItemById($itemId); + + if ($cartItem && $cartItem->getParentItemId()) { + throw new GraphQlInputException(__('Child items may not be updated.')); + } + + if (count($customizableOptions) === 0 && !isset($item['quantity'])) { + throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.')); + } + + $quantity = (float)$item['quantity']; + + if ($quantity <= 0.0) { + $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); + } else { + $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions); + } + + if (!empty($item['gift_message'])) { + try { + if (!$this->giftMessageHelper->isMessagesAllowed('items', $cartItem)) { + continue; + } + if (!$this->giftMessageHelper->isMessagesAllowed('item', $cartItem)) { + continue; + } + + /** @var MessageInterface $giftItemMessage */ + $giftItemMessage = $this->itemRepository->get($cart->getEntityId(), $itemId); + + if (empty($giftItemMessage)) { + /** @var MessageInterface $giftMessage */ + $giftMessage = $this->giftMessageFactory->create(); + $this->updateGiftMessageForItem($cart, $giftMessage, $item, $itemId); + continue; + } + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__('Gift Message cannot be updated.')); + } + + $this->updateGiftMessageForItem($cart, $giftItemMessage, $item, $itemId); + } + } + } + + /** + * Update Gift Message for Quote item + * + * @param Quote $cart + * @param MessageInterface $giftItemMessage + * @param array $item + * @param int $itemId + * + * @throws GraphQlInputException + */ + private function updateGiftMessageForItem(Quote $cart, MessageInterface $giftItemMessage, array $item, int $itemId) + { + try { + $giftItemMessage->setRecipient($item['gift_message']['to']); + $giftItemMessage->setSender($item['gift_message']['from']); + $giftItemMessage->setMessage($item['gift_message']['message']); + $this->itemRepository->save($cart->getEntityId(), $giftItemMessage, $itemId); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__('Gift Message cannot be updated')); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php index dd4ce8fe7f7a6..c2e4bfa44c9bb 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php @@ -71,14 +71,15 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + if (empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } - $maskedCartId = $args['input']['cart_id']; - if (!isset($args['input']['payment_method']['code']) || empty($args['input']['payment_method']['code'])) { + if (empty($args['input']['payment_method']['code'])) { throw new GraphQlInputException(__('Required parameter "code" for "payment_method" is missing.')); } + + $maskedCartId = $args['input']['cart_id']; $paymentData = $args['input']['payment_method']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php index fa90f08e4b553..005baaad0e1e5 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php @@ -14,53 +14,43 @@ use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Quote\Api\CartItemRepositoryInterface; use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Quote\Model\Quote; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; -use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem; +use Magento\QuoteGraphQl\Model\CartItem\DataProvider\UpdateCartItems as UpdateCartItemsProvider; /** * @inheritdoc */ class UpdateCartItems implements ResolverInterface { - /** - * @var UpdateCartItem - */ - private $updateCartItem; - /** * @var GetCartForUser */ private $getCartForUser; /** - * @var CartItemRepositoryInterface + * @var CartRepositoryInterface */ - private $cartItemRepository; + private $cartRepository; /** - * @var CartRepositoryInterface + * @var UpdateCartItemsProvider */ - private $cartRepository; + private $updateCartItems; /** - * @param GetCartForUser $getCartForUser - * @param CartItemRepositoryInterface $cartItemRepository - * @param UpdateCartItem $updateCartItem + * @param GetCartForUser $getCartForUser * @param CartRepositoryInterface $cartRepository + * @param UpdateCartItemsProvider $updateCartItems */ public function __construct( GetCartForUser $getCartForUser, - CartItemRepositoryInterface $cartItemRepository, - UpdateCartItem $updateCartItem, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + UpdateCartItemsProvider $updateCartItems ) { $this->getCartForUser = $getCartForUser; - $this->cartItemRepository = $cartItemRepository; - $this->updateCartItem = $updateCartItem; $this->cartRepository = $cartRepository; + $this->updateCartItems = $updateCartItems; } /** @@ -71,6 +61,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); } + $maskedCartId = $args['input']['cart_id']; if (empty($args['input']['cart_items']) @@ -78,13 +69,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value ) { throw new GraphQlInputException(__('Required parameter "cart_items" is missing.')); } - $cartItems = $args['input']['cart_items']; + $cartItems = $args['input']['cart_items']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); try { - $this->processCartItems($cart, $cartItems); + $this->updateCartItems->processCartItems($cart, $cartItems); $this->cartRepository->save($cart); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); @@ -98,39 +89,4 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value ], ]; } - - /** - * Process cart items - * - * @param Quote $cart - * @param array $items - * @throws GraphQlInputException - * @throws LocalizedException - */ - private function processCartItems(Quote $cart, array $items): void - { - foreach ($items as $item) { - if (empty($item['cart_item_id'])) { - throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.')); - } - $itemId = (int)$item['cart_item_id']; - $customizableOptions = $item['customizable_options'] ?? []; - - $cartItem = $cart->getItemById($itemId); - if ($cartItem && $cartItem->getParentItemId()) { - throw new GraphQlInputException(__('Child items may not be updated.')); - } - - if (count($customizableOptions) === 0 && !isset($item['quantity'])) { - throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.')); - } - $quantity = (float)$item['quantity']; - - if ($quantity <= 0.0) { - $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); - } else { - $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions); - } - } - } } diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json index 0652d39b5f426..25f089cf75a62 100644 --- a/app/code/Magento/QuoteGraphQl/composer.json +++ b/app/code/Magento/QuoteGraphQl/composer.json @@ -13,7 +13,8 @@ "magento/module-customer-graph-ql": "*", "magento/module-sales": "*", "magento/module-directory": "*", - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "*", + "magento/module-gift-message": "*" }, "suggest": { "magento/module-graph-ql-cache": "*" diff --git a/app/code/Magento/Sales/Model/Order/ItemRepository.php b/app/code/Magento/Sales/Model/Order/ItemRepository.php index 6e029ac468370..345fffc414fbc 100644 --- a/app/code/Magento/Sales/Model/Order/ItemRepository.php +++ b/app/code/Magento/Sales/Model/Order/ItemRepository.php @@ -167,10 +167,7 @@ public function deleteById($id) public function save(OrderItemInterface $entity) { if ($entity->getProductOption()) { - $request = $this->getBuyRequest($entity); - $productOptions = $entity->getProductOptions(); - $productOptions['info_buyRequest'] = $request->toArray(); - $entity->setProductOptions($productOptions); + $entity->setProductOptions($this->getItemProductOptions($entity)); } $this->metadata->getMapper()->save($entity); @@ -178,6 +175,23 @@ public function save(OrderItemInterface $entity) return $this->registry[$entity->getEntityId()]; } + /** + * Return product options + * + * @param OrderItemInterface $entity + * @return array + */ + private function getItemProductOptions(OrderItemInterface $entity): array + { + $request = $this->getBuyRequest($entity); + $productOptions = $entity->getProductOptions(); + $productOptions['info_buyRequest'] = $productOptions && !empty($productOptions['info_buyRequest']) + ? array_merge($productOptions['info_buyRequest'], $request->toArray()) + : $request->toArray(); + + return $productOptions; + } + /** * Set parent item. * diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php index 253dbd43fa580..6ddbce49829eb 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php @@ -80,11 +80,9 @@ public function draw() $lines = []; // draw Product name - $lines[0] = [ - [ + $lines[0][] = [ 'text' => $this->string->split($this->prepareText((string)$item->getName()), 35, true, true), 'feed' => 35 - ] ]; // draw SKU diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem/DataProvider.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem/DataProvider.php index 219ea45a330ca..20cdd7313b8ad 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem/DataProvider.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem/DataProvider.php @@ -225,9 +225,9 @@ private function getDiscountDetails(OrderInterface $associatedOrder, OrderItemIn $discounts = []; } else { $discounts [] = [ - 'label' => $associatedOrder->getDiscountDescription() ?? _('Discount'), + 'label' => $associatedOrder->getDiscountDescription() ?? __('Discount'), 'amount' => [ - 'value' => $orderItem->getDiscountAmount() ?? 0, + 'value' => abs($orderItem->getDiscountAmount()) ?? 0, 'currency' => $associatedOrder->getOrderCurrencyCode() ] ]; diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php index 6251a7dd07c7d..6f7b943bf6ca2 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php @@ -11,9 +11,11 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Sales\Api\Data\OrderExtensionInterface; use Magento\Sales\Api\Data\OrderInterface; +/** + * Resolve order totals taxes and discounts for order + */ class OrderTotal implements ResolverInterface { /** @@ -33,21 +35,13 @@ public function resolve( /** @var OrderInterface $order */ $order = $value['model']; $currency = $order->getOrderCurrencyCode(); - $extensionAttributes = $order->getExtensionAttributes(); - - $allAppliedTaxesForItemsData = $this->getAllAppliedTaxesForItems( - $extensionAttributes->getItemAppliedTaxes() ?? [] - ); - $appliedShippingTaxesForItemsData = $this->getAppliedShippingTaxesForItems( - $extensionAttributes->getItemAppliedTaxes() ?? [] - ); return [ 'base_grand_total' => ['value' => $order->getBaseGrandTotal(), 'currency' => $currency], 'grand_total' => ['value' => $order->getGrandTotal(), 'currency' => $currency], 'subtotal' => ['value' => $order->getSubtotal(), 'currency' => $currency], 'total_tax' => ['value' => $order->getTaxAmount(), 'currency' => $currency], - 'taxes' => $this->getAppliedTaxesDetails($order, $allAppliedTaxesForItemsData), + 'taxes' => $this->getAppliedTaxesDetails($order), 'discounts' => $this->getDiscountDetails($order), 'total_shipping' => ['value' => $order->getShippingAmount(), 'currency' => $currency], 'shipping_handling' => [ @@ -63,54 +57,55 @@ public function resolve( 'value' => $order->getShippingAmount(), 'currency' => $currency ], - 'taxes' => $this->getAppliedTaxesDetails($order, $appliedShippingTaxesForItemsData), + 'taxes' => $this->getAppliedShippingTaxesDetails($order), 'discounts' => $this->getShippingDiscountDetails($order), ] ]; } /** - * Retrieve applied taxes that apply to items + * Retrieve applied taxes that apply to the order * - * @param \Magento\Tax\Api\Data\OrderTaxDetailsItemInterface[] $itemAppliedTaxes + * @param OrderInterface $order * @return array */ - private function getAllAppliedTaxesForItems(array $itemAppliedTaxes): array + private function getAllAppliedTaxesOnOrders(OrderInterface $order): array { - $allAppliedTaxesForItemsData = []; - foreach ($itemAppliedTaxes as $taxItemIndex => $appliedTaxForItem) { - foreach ($appliedTaxForItem->getAppliedTaxes() ?? [] as $taxLineItem) { - $allAppliedTaxesForItemsData[$taxItemIndex][$taxItemIndex] = [ - 'title' => $taxLineItem->getDataByKey('title'), - 'percent' => $taxLineItem->getDataByKey('percent'), - 'amount' => $taxLineItem->getDataByKey('amount'), - ]; - } + $extensionAttributes = $order->getExtensionAttributes(); + $appliedTaxes = $extensionAttributes->getAppliedTaxes() ?? []; + $allAppliedTaxOnOrders = []; + foreach ($appliedTaxes as $taxIndex => $appliedTaxesData) { + $allAppliedTaxOnOrders[$taxIndex] = [ + 'title' => $appliedTaxesData->getDataByKey('title'), + 'percent' => $appliedTaxesData->getDataByKey('percent'), + 'amount' => $appliedTaxesData->getDataByKey('amount'), + ]; } - return $allAppliedTaxesForItemsData; + return $allAppliedTaxOnOrders; } /** - * Retrieve applied taxes that apply to shipping + * Return taxes applied to the current order * - * @param \Magento\Tax\Api\Data\OrderTaxDetailsItemInterface $itemAppliedTaxes + * @param OrderInterface $order * @return array */ - private function getAppliedShippingTaxesForItems(array $itemAppliedTaxes): array + private function getAppliedTaxesDetails(OrderInterface $order): array { - $appliedShippingTaxesForItemsData = []; - foreach ($itemAppliedTaxes as $taxItemIndex => $appliedTaxForItem) { - foreach ($appliedTaxForItem->getAppliedTaxes() ?? [] as $taxLineItem) { - if ($appliedTaxForItem->getType() === "shipping") { - $appliedShippingTaxesForItemsData[$taxItemIndex][$taxItemIndex] = [ - 'title' => $taxLineItem->getDataByKey('title'), - 'percent' => $taxLineItem->getDataByKey('percent'), - 'amount' => $taxLineItem->getDataByKey('amount') - ]; - } - } + $allAppliedTaxOnOrders = $this->getAllAppliedTaxesOnOrders($order); + $taxes = []; + foreach ($allAppliedTaxOnOrders as $appliedTaxes) { + $appliedTaxesArray = [ + 'rate' => $appliedTaxes['percent'] ?? 0, + 'title' => $appliedTaxes['title'] ?? null, + 'amount' => [ + 'value' => $appliedTaxes['amount'] ?? 0, + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + $taxes[] = $appliedTaxesArray; } - return $appliedShippingTaxesForItemsData; + return $taxes; } /** @@ -119,66 +114,91 @@ private function getAppliedShippingTaxesForItems(array $itemAppliedTaxes): array * @param OrderInterface $order * @return array */ - private function getShippingDiscountDetails(OrderInterface $order) + private function getDiscountDetails(OrderInterface $order): array { - $shippingDiscounts = []; - if (!($order->getDiscountDescription() === null && $order->getShippingDiscountAmount() == 0)) { - $shippingDiscounts[] = - [ - 'label' => $order->getDiscountDescription() ?? __('Discount'), - 'amount' => [ - 'value' => $order->getShippingDiscountAmount(), - 'currency' => $order->getOrderCurrencyCode() - ] - ]; + $orderDiscounts = []; + if (!($order->getDiscountDescription() === null && $order->getDiscountAmount() == 0)) { + $orderDiscounts[] = [ + 'label' => $order->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($order->getDiscountAmount()), + 'currency' => $order->getOrderCurrencyCode() + ] + ]; } - return $shippingDiscounts; + return $orderDiscounts; } /** - * Return information about an applied discount + * Retrieve applied shipping taxes on items for the orders * * @param OrderInterface $order * @return array */ - private function getDiscountDetails(OrderInterface $order) + private function getAppliedShippingTaxesForItems(OrderInterface $order): array { - $discounts = []; - if (!($order->getDiscountDescription() === null && $order->getDiscountAmount() == 0)) { - $discounts[] = [ - 'label' => $order->getDiscountDescription() ?? __('Discount'), + $extensionAttributes = $order->getExtensionAttributes(); + $itemAppliedTaxes = $extensionAttributes->getItemAppliedTaxes() ?? []; + $appliedShippingTaxesForItems = []; + foreach ($itemAppliedTaxes as $appliedTaxForItem) { + if ($appliedTaxForItem->getType() === "shipping") { + foreach ($appliedTaxForItem->getAppliedTaxes() ?? [] as $taxLineItem) { + $taxItemIndexTitle = $taxLineItem->getDataByKey('title'); + $appliedShippingTaxesForItems[$taxItemIndexTitle] = [ + 'title' => $taxLineItem->getDataByKey('title'), + 'percent' => $taxLineItem->getDataByKey('percent'), + 'amount' => $taxLineItem->getDataByKey('amount') + ]; + } + } + } + return $appliedShippingTaxesForItems; + } + + /** + * Return taxes applied to the current order + * + * @param OrderInterface $order + * @return array + */ + private function getAppliedShippingTaxesDetails( + OrderInterface $order + ): array { + $appliedShippingTaxesForItems = $this->getAppliedShippingTaxesForItems($order); + $shippingTaxes = []; + foreach ($appliedShippingTaxesForItems as $appliedShippingTaxes) { + $appliedShippingTaxesArray = [ + 'rate' => $appliedShippingTaxes['percent'] ?? 0, + 'title' => $appliedShippingTaxes['title'] ?? null, 'amount' => [ - 'value' => $order->getDiscountAmount(), + 'value' => $appliedShippingTaxes['amount'] ?? 0, 'currency' => $order->getOrderCurrencyCode() ] ]; + $shippingTaxes[] = $appliedShippingTaxesArray; } - return $discounts; + return $shippingTaxes; } /** - * Returns taxes applied to the current order + * Return information about an applied discount * * @param OrderInterface $order - * @param array $appliedTaxesArray * @return array */ - private function getAppliedTaxesDetails(OrderInterface $order, array $appliedTaxesArray): array + private function getShippingDiscountDetails(OrderInterface $order): array { - $taxes = []; - foreach ($appliedTaxesArray as $appliedTaxesKeyIndex => $appliedTaxes) { - $appliedTaxesArray = [ - 'title' => $appliedTaxes[$appliedTaxesKeyIndex]['title'] ?? null, - 'amount' => [ - 'value' => $appliedTaxes[$appliedTaxesKeyIndex]['amount'] ?? 0, - 'currency' => $order->getOrderCurrencyCode() - ], - ]; - if (!empty($appliedTaxes[$appliedTaxesKeyIndex])) { - $appliedTaxesArray['rate'] = $appliedTaxes[$appliedTaxesKeyIndex]['percent'] ?? null; - } - $taxes[] = $appliedTaxesArray; + $shippingDiscounts = []; + if (!($order->getDiscountDescription() === null && $order->getShippingDiscountAmount() == 0)) { + $shippingDiscounts[] = + [ + 'label' => $order->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($order->getShippingDiscountAmount()), + 'currency' => $order->getOrderCurrencyCode() + ] + ]; } - return $taxes; + return $shippingDiscounts; } } diff --git a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php index 537fec4c75df6..8f6011f1ae56f 100644 --- a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php +++ b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php @@ -6,7 +6,7 @@ namespace Magento\Store\Api\Data; /** - * StoreConfig interface + * Interface for store config * * @api * @since 100.0.2 @@ -141,7 +141,7 @@ public function setWeightUnit($weightUnit); public function getBaseUrl(); /** - * set base URL + * Set base URL * * @param string $baseUrl * @return $this @@ -201,7 +201,7 @@ public function setBaseMediaUrl($baseMediaUrl); public function getSecureBaseUrl(); /** - * set secure base URL + * Set secure base URL * * @param string $secureBaseUrl * @return $this diff --git a/app/code/Magento/Store/Model/Data/StoreConfig.php b/app/code/Magento/Store/Model/Data/StoreConfig.php index 6634e2cb05bd9..e68d98b162613 100644 --- a/app/code/Magento/Store/Model/Data/StoreConfig.php +++ b/app/code/Magento/Store/Model/Data/StoreConfig.php @@ -6,7 +6,7 @@ namespace Magento\Store\Model\Data; /** - * Class StoreConfig + * Allows to get and set store config values * * @codeCoverageIgnore */ @@ -188,7 +188,7 @@ public function getBaseUrl() } /** - * set base URL + * Set base URL * * @param string $baseUrl * @return $this @@ -293,7 +293,7 @@ public function getSecureBaseUrl() } /** - * set secure base URL + * Set secure base URL * * @param string $secureBaseUrl * @return $this @@ -367,7 +367,7 @@ public function setSecureBaseMediaUrl($secureBaseMediaUrl) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Store\Api\Data\StoreConfigExtensionInterface|null */ @@ -377,7 +377,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Store\Api\Data\StoreConfigExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Store/Model/Service/StoreConfigManager.php b/app/code/Magento/Store/Model/Service/StoreConfigManager.php index b3c2208a58361..debb08438a3b4 100644 --- a/app/code/Magento/Store/Model/Service/StoreConfigManager.php +++ b/app/code/Magento/Store/Model/Service/StoreConfigManager.php @@ -5,6 +5,9 @@ */ namespace Magento\Store\Model\Service; +/** + * Allows to get store config + */ class StoreConfigManager implements \Magento\Store\Api\StoreConfigManagerInterface { /** @@ -53,6 +56,8 @@ public function __construct( } /** + * Get store configs + * * @param string[] $storeCodes list of stores by store codes, will return all if storeCodes is not set * @return \Magento\Store\Api\Data\StoreConfigInterface[] */ @@ -71,6 +76,8 @@ public function getStoreConfigs(array $storeCodes = null) } /** + * Get store config + * * @param \Magento\Store\Model\Store $store * @return \Magento\Store\Api\Data\StoreConfigInterface */ diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 5bd8f6e2349fc..2da9e91e1fddd 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -65,7 +65,6 @@ <preference for="Magento\Framework\App\Router\PathConfigInterface" type="Magento\Store\Model\PathConfig" /> <type name="Magento\Framework\App\ActionInterface"> <plugin name="storeCheck" type="Magento\Store\App\Action\Plugin\StoreCheck"/> - <plugin name="designLoader" type="Magento\Framework\App\Action\Plugin\LoadDesignPlugin"/> <plugin name="eventDispatch" type="Magento\Framework\App\Action\Plugin\EventDispatchPlugin"/> <plugin name="actionFlagNoDispatch" type="Magento\Framework\App\Action\Plugin\ActionFlagNoDispatchPlugin"/> </type> diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php index 59f9831789a35..0baee00f468a0 100644 --- a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php @@ -55,11 +55,10 @@ public function __construct( */ public function getStoreConfigData(StoreInterface $store): array { - $storeConfigData = array_merge( + return array_merge( $this->getBaseConfigData($store), $this->getExtendedConfigData((int)$store->getId()) ); - return $storeConfigData; } /** @@ -72,7 +71,7 @@ private function getBaseConfigData(StoreInterface $store) : array { $storeConfig = current($this->storeConfigManager->getStoreConfigs([$store->getCode()])); - $storeConfigData = [ + return [ 'id' => $storeConfig->getId(), 'code' => $storeConfig->getCode(), 'website_id' => $storeConfig->getWebsiteId(), @@ -88,9 +87,9 @@ private function getBaseConfigData(StoreInterface $store) : array 'secure_base_url' => $storeConfig->getSecureBaseUrl(), 'secure_base_link_url' => $storeConfig->getSecureBaseLinkUrl(), 'secure_base_static_url' => $storeConfig->getSecureBaseStaticUrl(), - 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl() + 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl(), + 'store_name' => $store->getName() ]; - return $storeConfigData; } /** diff --git a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml index f3771b704c3e9..3a0143821d8b9 100644 --- a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml @@ -23,11 +23,4 @@ </argument> </arguments> </type> - <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> - <arguments> - <argument name="extendedConfigData" xsi:type="array"> - <item name="store_name" xsi:type="string">store/information/name</item> - </argument> - </arguments> - </type> </config> diff --git a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php similarity index 88% rename from lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php rename to app/code/Magento/Theme/Plugin/LoadDesignPlugin.php index 2cda49c43c2ce..c4f8d3a905d0f 100644 --- a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php +++ b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -namespace Magento\Framework\App\Action\Plugin; +namespace Magento\Theme\Plugin; use Magento\Framework\App\ActionInterface; use Magento\Framework\Config\Dom\ValidationException; @@ -21,12 +21,12 @@ class LoadDesignPlugin /** * @var DesignLoader */ - protected $_designLoader; + private $designLoader; /** * @var MessageManagerInterface */ - protected $messageManager; + private $messageManager; /** * @param DesignLoader $designLoader @@ -36,7 +36,7 @@ public function __construct( DesignLoader $designLoader, MessageManagerInterface $messageManager ) { - $this->_designLoader = $designLoader; + $this->designLoader = $designLoader; $this->messageManager = $messageManager; } @@ -50,7 +50,7 @@ public function __construct( public function beforeExecute(ActionInterface $subject) { try { - $this->_designLoader->load(); + $this->designLoader->load(); } catch (LocalizedException $e) { if ($e->getPrevious() instanceof ValidationException) { /** @var MessageInterface $message */ diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php similarity index 80% rename from lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php rename to app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php index 549d45a986cf0..4efcc584986d1 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php +++ b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php @@ -3,15 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); - -namespace Magento\Framework\App\Test\Unit\Action\Plugin; +namespace Magento\Theme\Test\Unit\Plugin; use Magento\Framework\App\Action\Action; -use Magento\Framework\App\Action\Plugin\LoadDesignPlugin; use Magento\Framework\App\ActionInterface; use Magento\Framework\Message\ManagerInterface; use Magento\Framework\View\DesignLoader; +use Magento\Theme\Plugin\LoadDesignPlugin; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -26,7 +24,7 @@ public function testBeforeExecute() $designLoaderMock = $this->createMock(DesignLoader::class); /** @var MockObject|ManagerInterface $messageManagerMock */ - $messageManagerMock = $this->getMockForAbstractClass(ManagerInterface::class); + $messageManagerMock = $this->createMock(ManagerInterface::class); $plugin = new LoadDesignPlugin($designLoaderMock, $messageManagerMock); diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml index c4da1f860870e..15107adb931c9 100644 --- a/app/code/Magento/Theme/etc/di.xml +++ b/app/code/Magento/Theme/etc/di.xml @@ -105,6 +105,9 @@ <argument name="scope" xsi:type="const">Magento\Store\Model\ScopeInterface::SCOPE_STORE</argument> </arguments> </virtualType> + <type name="Magento\Framework\App\ActionInterface"> + <plugin name="designLoader" type="Magento\Theme\Plugin\LoadDesignPlugin"/> + </type> <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> <arguments> <argument name="collections" xsi:type="array"> diff --git a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml index 55798169cdf75..b42cabde6cd85 100644 --- a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml @@ -14,7 +14,6 @@ <script> require([ 'jquery', - 'jquery-ui-modules/datepicker' ], function($){ //<![CDATA[ @@ -34,7 +33,7 @@ require([ timeText: "<?= $block->escapeJs(__('Time')) ?>", hourText: "<?= $block->escapeJs(__('Hour')) ?>", minuteText: "<?= $block->escapeJs(__('Minute')) ?>", - dateFormat: $.datepicker.RFC_2822, + dateFormat: "D, d M yy", // $.datepicker.RFC_2822 showOn: "button", showAnim: "", changeMonth: true, diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js index 2fab8c219c02a..284d395d8120b 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js @@ -7,11 +7,8 @@ define([ 'ko', 'underscore', 'jquery', - 'mage/translate', - 'mage/calendar', - 'moment', - 'mageUtils' -], function (ko, _, $, $t, calendar, moment, utils) { + 'mage/translate' +], function (ko, _, $, $t) { 'use strict'; var defaults = { @@ -46,10 +43,12 @@ define([ observable = config; } - $(el).calendar(options); + require(['mage/calendar'], function () { + $(el).calendar(options); - ko.utils.registerEventHandler(el, 'change', function () { - observable(this.value); + ko.utils.registerEventHandler(el, 'change', function () { + observable(this.value); + }); }); }, @@ -62,6 +61,7 @@ define([ */ update: function (element, valueAccessor) { var config = valueAccessor(), + $element = $(element), observable, options = {}, newVal; @@ -75,26 +75,21 @@ define([ observable = config; } - if (_.isEmpty(observable())) { - if ($(element).datepicker('getDate')) { - $(element).datepicker('setDate', null); - $(element).blur(); + require(['moment', 'mage/utils/misc', 'mage/calendar'], function (moment, utils) { + if (_.isEmpty(observable())) { + newVal = null; + } else { + newVal = moment( + observable(), + utils.convertToMomentFormat( + options.dateFormat + (options.showsTime ? ' ' + options.timeFormat : '') + ) + ).toDate(); } - } else { - newVal = moment( - observable(), - utils.convertToMomentFormat( - options.dateFormat + (options.showsTime ? ' ' + options.timeFormat : '') - ) - ).toDate(); - if ($(element).datepicker('getDate') == null || - newVal.valueOf() !== $(element).datepicker('getDate').valueOf() - ) { - $(element).datepicker('setDate', newVal); - $(element).blur(); - } - } + $element.datepicker('setDate', newVal); + $element.blur(); + }); } }; }); diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js index 1dda3254f4613..52031dc0c3792 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js @@ -7,13 +7,18 @@ define([ 'ko', 'jquery', 'underscore', - '../template/renderer', - 'jquery-ui-modules/slider' + '../template/renderer' ], function (ko, $, _, renderer) { 'use strict'; var isTouchDevice = !_.isUndefined(document.ontouchstart), - sliderFn = 'slider'; + sliderFn = 'slider', + sliderModule = 'jquery-ui-modules/slider'; + + if (isTouchDevice) { + sliderFn = 'touchSlider'; + sliderModule = 'mage/touch-slider'; + } ko.bindingHandlers.range = { @@ -41,7 +46,9 @@ define([ } }); - $(element)[sliderFn](config); + require([sliderModule], function () { + $(element)[sliderFn](config); + }); }, /** @@ -55,149 +62,11 @@ define([ config.value = ko.unwrap(config.value); - $(element)[sliderFn]('option', config); + require([sliderModule], function () { + $(element)[sliderFn]('option', config); + }); } }; renderer.addAttribute('range'); - - if (!isTouchDevice) { - return; - } - - $.widget('mage.touchSlider', $.ui.slider, { - - /** - * Creates instance of widget. - * - * @override - */ - _create: function () { - _.bindAll( - this, - '_mouseDown', - '_mouseMove', - '_onTouchEnd' - ); - - return this._superApply(arguments); - }, - - /** - * Initializes mouse events on element. - * @override - */ - _mouseInit: function () { - var result = this._superApply(arguments); - - this.element - .off('mousedown.' + this.widgetName) - .on('touchstart.' + this.widgetName, this._mouseDown); - - return result; - }, - - /** - * Elements' 'mousedown' event handler polyfill. - * @override - */ - _mouseDown: function (event) { - var prevDelegate = this._mouseMoveDelegate, - result; - - event = this._touchToMouse(event); - result = this._super(event); - - if (prevDelegate === this._mouseMoveDelegate) { - return result; - } - - $(document) - .off('mousemove.' + this.widgetName) - .off('mouseup.' + this.widgetName); - - $(document) - .on('touchmove.' + this.widgetName, this._mouseMove) - .on('touchend.' + this.widgetName, this._onTouchEnd) - .on('tochleave.' + this.widgetName, this._onTouchEnd); - - return result; - }, - - /** - * Documents' 'mousemove' event handler polyfill. - * - * @override - * @param {Event} event - Touch event object. - */ - _mouseMove: function (event) { - event = this._touchToMouse(event); - - return this._super(event); - }, - - /** - * Documents' 'touchend' event handler. - */ - _onTouchEnd: function (event) { - $(document).trigger('mouseup'); - - return this._mouseUp(event); - }, - - /** - * Removes previously assigned touch handlers. - * - * @override - */ - _mouseUp: function () { - this._removeTouchHandlers(); - - return this._superApply(arguments); - }, - - /** - * Removes previously assigned touch handlers. - * - * @override - */ - _mouseDestroy: function () { - this._removeTouchHandlers(); - - return this._superApply(arguments); - }, - - /** - * Removes touch events from document object. - */ - _removeTouchHandlers: function () { - $(document) - .off('touchmove.' + this.widgetName) - .off('touchend.' + this.widgetName) - .off('touchleave.' + this.widgetName); - }, - - /** - * Adds properties to the touch event to mimic mouse event. - * - * @param {Event} event - Touch event object. - * @returns {Event} - */ - _touchToMouse: function (event) { - var orig = event.originalEvent, - touch = orig.touches[0]; - - return _.extend(event, { - which: 1, - pageX: touch.pageX, - pageY: touch.pageY, - clientX: touch.clientX, - clientY: touch.clientY, - screenX: touch.screenX, - screenY: touch.screenY - }); - } - }); - - sliderFn = 'touchSlider'; }); diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml index 9d6b267055f70..a1adb918d0e8d 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml @@ -44,34 +44,31 @@ </after> <!-- 1. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue2"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewrite"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInGrid"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInGrid"> + <argument name="requestPath" value="$createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> <!-- 2. Set the configuration for Generate "category/product" URL Rewrites to No--> - <amOnPage url="{{CatalogConfigPage.url}}" stepKey="amOnCatalogConfigPage"/> - <conditionalClick selector="{{CatalogSection.seo}}" dependentSelector="{{CatalogSection.CheckIfSeoTabExpand}}" visible="true" stepKey="expandSeoTab" /> - <waitForElementVisible selector="{{CatalogSection.GenerateUrlRewrites}}" stepKey="GenerateUrlRewritesSelect"/> - <selectOption userInput="0" selector="{{CatalogSection.GenerateUrlRewrites}}" stepKey="selectUrlGenerationNo" /> - <waitForElementVisible selector="{{GenerateUrlRewritesConfirm.title}}" stepKey="waitForConfirmModal"/> - <click selector="{{GenerateUrlRewritesConfirm.ok}}" stepKey="confirmSwitchingGenerationOff"/> - <click selector="{{CatalogSection.save}}" stepKey="saveConfig" /> - <waitForPageLoad stepKey="waitForSavingSystemConfiguration"/> + <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> <!-- 3. Flush cache--> <magentoCLI command="cache:flush" stepKey="cleanCache"/> <!-- 4. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage2"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName2"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue1"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="dontSeeValue2"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteAfterDisablingTheConfig"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInGridAfterDisablingTheConfig"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="categoryUrlIsNotShownAfterDisablingTheConfig"> + <argument name="requestPath" value="$createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php new file mode 100644 index 0000000000000..7acfb503a5ad0 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php @@ -0,0 +1,164 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Adding products to wishlist + */ +class AddProductsToWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND'; + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var BuyRequestBuilder + */ + private $buyRequestBuilder; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @param ProductRepositoryInterface $productRepository + * @param BuyRequestBuilder $buyRequestBuilder + * @param WishlistResourceModel $wishlistResource + */ + public function __construct( + ProductRepositoryInterface $productRepository, + BuyRequestBuilder $buyRequestBuilder, + WishlistResourceModel $wishlistResource + ) { + $this->productRepository = $productRepository; + $this->buyRequestBuilder = $buyRequestBuilder; + $this->wishlistResource = $wishlistResource; + } + + /** + * Adding products to wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItems + * + * @return WishlistOutput + * + * @throws AlreadyExistsException + */ + public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutput + { + foreach ($wishlistItems as $wishlistItem) { + $this->addItemToWishlist($wishlist, $wishlistItem); + } + + $wishlistOutput = $this->prepareOutput($wishlist); + + if ($wishlist->isObjectNew() || count($wishlistOutput->getErrors()) !== count($wishlistItems)) { + $this->wishlistResource->save($wishlist); + } + + return $wishlistOutput; + } + + /** + * Add product item to wishlist + * + * @param Wishlist $wishlist + * @param WishlistItem $wishlistItem + * + * @return void + */ + private function addItemToWishlist(Wishlist $wishlist, WishlistItem $wishlistItem): void + { + $sku = $wishlistItem->getParentSku() ?? $wishlistItem->getSku(); + + try { + $product = $this->productRepository->get($sku, false, null, true); + } catch (NoSuchEntityException $e) { + $this->addError( + __('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(), + self::ERROR_PRODUCT_NOT_FOUND + ); + + return; + } + + try { + $options = $this->buyRequestBuilder->build($wishlistItem, (int) $product->getId()); + $result = $wishlist->addNewItem($product, $options); + + if (is_string($result)) { + $this->addError($result); + } + } catch (LocalizedException $exception) { + $this->addError($exception->getMessage()); + } catch (\Throwable $e) { + $this->addError( + __( + 'Could not add the product with SKU "%sku" to the wishlist:: %message', + ['sku' => $sku, 'message' => $e->getMessage()] + )->render() + ); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php new file mode 100644 index 0000000000000..1cfa316c3cd01 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for bundle product buy requests + */ +class BundleDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'bundle'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItem, ?int $productId): array + { + $bundleOptionsData = []; + + foreach ($wishlistItem->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $optionId, $optionValueId, $optionQuantity] = $optionData; + + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } + + return $bundleOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php new file mode 100644 index 0000000000000..1f7ddce345b1c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Building buy request for all product types + */ +class BuyRequestBuilder +{ + /** + * @var BuyRequestDataProviderInterface[] + */ + private $providers; + + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + + /** + * @param DataObjectFactory $dataObjectFactory + * @param array $providers + */ + public function __construct( + DataObjectFactory $dataObjectFactory, + array $providers = [] + ) { + $this->dataObjectFactory = $dataObjectFactory; + $this->providers = $providers; + } + + /** + * Build product buy request for adding to wishlist + * + * @param WishlistItem $wishlistItemData + * @param int|null $productId + * + * @return DataObject + */ + public function build(WishlistItem $wishlistItemData, ?int $productId = null): DataObject + { + $requestData = [ + [ + 'qty' => $wishlistItemData->getQuantity(), + ] + ]; + + foreach ($this->providers as $provider) { + $requestData[] = $provider->execute($wishlistItemData, $productId); + } + + return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php new file mode 100644 index 0000000000000..fac45d7f86c7c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Build buy request for adding products to wishlist + */ +interface BuyRequestDataProviderInterface +{ + /** + * Provide buy request data from add to wishlist item request + * + * @param WishlistItem $wishlistItemData + * @param int|null $productId + * + * @return array + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array; +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.php new file mode 100644 index 0000000000000..e8f5bf0654f64 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for custom options buy requests + */ +class CustomizableOptionDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'custom-option'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $customizableOptionsData = []; + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $optionId, $optionValue] = $optionData; + + $customizableOptionsData[$optionId][] = $optionValue; + } + + foreach ($wishlistItemData->getEnteredOptions() as $option) { + $optionData = \explode('/', base64_decode($option->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $optionId] = $optionData; + + $customizableOptionsData[$optionId][] = $option->getValue(); + } + + if (empty($customizableOptionsData)) { + return $customizableOptionsData; + } + + $result = ['options' => $this->flattenOptionValues($customizableOptionsData)]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Flatten option values for non-multiselect customizable options + * + * @param array $customizableOptionsData + * + * @return array + */ + private function flattenOptionValues(array $customizableOptionsData): array + { + foreach ($customizableOptionsData as $optionId => $optionValue) { + if (count($optionValue) === 1) { + $customizableOptionsData[$optionId] = $optionValue[0]; + } + } + + return $customizableOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php new file mode 100644 index 0000000000000..1ad1a0b64706a --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for downloadable product buy requests + */ +class DownloadableLinkDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'downloadable'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItem, ?int $productId): array + { + $linksData = []; + + foreach ($wishlistItem->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $linkId] = $optionData; + + $linksData[] = $linkId; + } + + return $linksData ? ['links' => $linksData] : []; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php new file mode 100644 index 0000000000000..01e29bcf80c0b --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for configurable product buy requests + */ +class SuperAttributeDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'configurable'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $configurableData = []; + + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $attributeId, $valueIndex] = $optionData; + + $configurableData[$attributeId] = $valueIndex; + } + + if (empty($configurableData)) { + return $configurableData; + } + + $result = ['super_attribute' => $configurableData]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php new file mode 100644 index 0000000000000..a11f631f1f5aa --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for grouped product buy requests + */ +class SuperGroupDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'grouped'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $groupedData = []; + + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $simpleProductId, $quantity] = $optionData; + + $groupedData[$simpleProductId] = $quantity; + } + + if (empty($groupedData)) { + return $groupedData; + } + + $result = ['super_group' => $groupedData]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Config.php b/app/code/Magento/Wishlist/Model/Wishlist/Config.php new file mode 100644 index 0000000000000..041e9f1ca0a21 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Config.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Provides wishlist configuration + */ +class Config +{ + const XML_PATH_WISHLIST_ACTIVE = 'wishlist/general/active'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check whether the wishlist is enabled or not + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_WISHLIST_ACTIVE, + ScopeInterface::SCOPE_STORES + ); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php new file mode 100644 index 0000000000000..0d6b2a2302540 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents entered options + */ +class EnteredOption +{ + /** + * @var string + */ + private $id; + + /** + * @var string + */ + private $value; + + /** + * @param string $id + * @param string $value + */ + public function __construct(string $id, string $value) + { + $this->id = $id; + $this->value = $value; + } + + /** + * Get entered option id + * + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * Get entered option value + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php new file mode 100644 index 0000000000000..cb8420169fa8a --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents error item + */ +class Error +{ + /** + * @var string + */ + private $message; + + /** + * @var string + */ + private $code; + + /** + * @param string $message + * @param string $code + */ + public function __construct(string $message, string $code) + { + $this->message = $message; + $this->code = $code; + } + + /** + * Get error message + * + * @return string + */ + public function getMessage(): string + { + return $this->message; + } + + /** + * Get error code + * + * @return string + */ + public function getCode(): string + { + return $this->code; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php new file mode 100644 index 0000000000000..129a61c0a2a6c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents selected option + */ +class SelectedOption +{ + /** + * @var string + */ + private $id; + + /** + * @param string $id + */ + public function __construct(string $id) + { + $this->id = $id; + } + + /** + * Get selected option id + * + * @return string + */ + public function getId(): string + { + return $this->id; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php new file mode 100644 index 0000000000000..236b7f1eee72d --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents Wishlist Item data + */ +class WishlistItem +{ + /** + * @var float + */ + private $quantity; + + /** + * @var string|null + */ + private $sku; + + /** + * @var string + */ + private $parentSku; + + /** + * @var int|null + */ + private $id; + + /** + * @var string|null + */ + private $description; + + /** + * @var SelectedOption[] + */ + private $selectedOptions; + + /** + * @var EnteredOption[] + */ + private $enteredOptions; + + /** + * @param float $quantity + * @param string|null $sku + * @param string|null $parentSku + * @param int|null $id + * @param string|null $description + * @param array|null $selectedOptions + * @param array|null $enteredOptions + */ + public function __construct( + float $quantity, + string $sku = null, + string $parentSku = null, + int $id = null, + string $description = null, + array $selectedOptions = null, + array $enteredOptions = null + ) { + $this->quantity = $quantity; + $this->sku = $sku; + $this->parentSku = $parentSku; + $this->id = $id; + $this->description = $description; + $this->selectedOptions = $selectedOptions; + $this->enteredOptions = $enteredOptions; + } + + /** + * Get wishlist item id + * + * @return int|null + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * Get wishlist item description + * + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * Get sku + * + * @return string|null + */ + public function getSku(): ?string + { + return $this->sku; + } + + /** + * Get quantity + * + * @return float + */ + public function getQuantity(): float + { + return $this->quantity; + } + + /** + * Get parent sku + * + * @return string|null + */ + public function getParentSku(): ?string + { + return $this->parentSku; + } + + /** + * Get selected options + * + * @return SelectedOption[]|null + */ + public function getSelectedOptions(): ?array + { + return $this->selectedOptions; + } + + /** + * Get entered options + * + * @return EnteredOption[]|null + */ + public function getEnteredOptions(): ?array + { + return $this->enteredOptions; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php new file mode 100644 index 0000000000000..153e8451bae31 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +use Magento\Framework\Exception\InputException; + +/** + * Create WishlistItem DTO + */ +class WishlistItemFactory +{ + /** + * Create wishlist item DTO + * + * @param array $data + * + * @return WishlistItem + */ + public function create(array $data): WishlistItem + { + return new WishlistItem( + $data['quantity'], + $data['sku'] ?? null, + $data['parent_sku'] ?? null, + isset($data['wishlist_item_id']) ? (int) $data['wishlist_item_id'] : null, + $data['description'] ?? null, + isset($data['selected_options']) ? $this->createSelectedOptions($data['selected_options']) : [], + isset($data['entered_options']) ? $this->createEnteredOptions($data['entered_options']) : [] + ); + } + + /** + * Create array of Entered Options + * + * @param array $options + * + * @return EnteredOption[] + */ + private function createEnteredOptions(array $options): array + { + return \array_map( + function (array $option) { + if (!isset($option['id'], $option['value'])) { + throw new InputException( + __('Required fields are not present EnteredOption.id, EnteredOption.value') + ); + } + return new EnteredOption($option['id'], $option['value']); + }, + $options + ); + } + + /** + * Create array of Selected Options + * + * @param string[] $options + * + * @return SelectedOption[] + */ + private function createSelectedOptions(array $options): array + { + return \array_map( + function ($option) { + return new SelectedOption($option); + }, + $options + ); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php new file mode 100644 index 0000000000000..fc7db9ec910fb --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +use Magento\Wishlist\Model\Wishlist; + +/** + * DTO represent output for \Magento\WishlistGraphQl\Model\Resolver\AddProductsToWishlistResolver + */ +class WishlistOutput +{ + /** + * @var Wishlist + */ + private $wishlist; + + /** + * @var Error[] + */ + private $errors; + + /** + * @param Wishlist $wishlist + * @param Error[] $errors + */ + public function __construct(Wishlist $wishlist, array $errors) + { + $this->wishlist = $wishlist; + $this->errors = $errors; + } + + /** + * Get Wishlist + * + * @return Wishlist + */ + public function getWishlist(): Wishlist + { + return $this->wishlist; + } + + /** + * Get errors happened during adding products to wishlist + * + * @return Error[] + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php new file mode 100644 index 0000000000000..d143830064752 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Wishlist\Model\Item as WishlistItem; +use Magento\Wishlist\Model\ItemFactory as WishlistItemFactory; +use Magento\Wishlist\Model\ResourceModel\Item as WishlistItemResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Remove product items from wishlist + */ +class RemoveProductsFromWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND'; + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var WishlistItemFactory + */ + private $wishlistItemFactory; + + /** + * @var WishlistItemResource + */ + private $wishlistItemResource; + + /** + * @param WishlistItemFactory $wishlistItemFactory + * @param WishlistItemResource $wishlistItemResource + */ + public function __construct( + WishlistItemFactory $wishlistItemFactory, + WishlistItemResource $wishlistItemResource + ) { + $this->wishlistItemFactory = $wishlistItemFactory; + $this->wishlistItemResource = $wishlistItemResource; + } + + /** + * Removing items from wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItemsIds + * + * @return WishlistOutput + */ + public function execute(Wishlist $wishlist, array $wishlistItemsIds): WishlistOutput + { + foreach ($wishlistItemsIds as $wishlistItemId) { + $this->removeItemFromWishlist((int) $wishlistItemId); + } + + return $this->prepareOutput($wishlist); + } + + /** + * Remove product item from wishlist + * + * @param int $wishlistItemId + * + * @return void + */ + private function removeItemFromWishlist(int $wishlistItemId): void + { + try { + /** @var WishlistItem $wishlistItem */ + $wishlistItem = $this->wishlistItemFactory->create(); + $this->wishlistItemResource->load($wishlistItem, $wishlistItemId); + if (!$wishlistItem->getId()) { + $this->addError( + __('Could not find a wishlist item with ID "%id"', ['id' => $wishlistItemId])->render(), + self::ERROR_PRODUCT_NOT_FOUND + ); + } + + $this->wishlistItemResource->delete($wishlistItem); + } catch (\Exception $e) { + $this->addError( + __( + 'We can\'t delete the item with ID "%id" from the Wish List right now.', + ['id' => $wishlistItemId] + )->render() + ); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php new file mode 100644 index 0000000000000..4abcada138362 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Wishlist\Model\Item as WishlistItem; +use Magento\Wishlist\Model\ItemFactory as WishlistItemFactory; +use Magento\Wishlist\Model\ResourceModel\Item as WishlistItemResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem as WishlistItemData; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Updating product items in wishlist + */ +class UpdateProductsInWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var BuyRequestBuilder + */ + private $buyRequestBuilder; + + /** + * @var WishlistItemFactory + */ + private $wishlistItemFactory; + + /** + * @var WishlistItemResource + */ + private $wishlistItemResource; + + /** + * @param BuyRequestBuilder $buyRequestBuilder + * @param WishlistItemFactory $wishlistItemFactory + * @param WishlistItemResource $wishlistItemResource + */ + public function __construct( + BuyRequestBuilder $buyRequestBuilder, + WishlistItemFactory $wishlistItemFactory, + WishlistItemResource $wishlistItemResource + ) { + $this->buyRequestBuilder = $buyRequestBuilder; + $this->wishlistItemFactory = $wishlistItemFactory; + $this->wishlistItemResource = $wishlistItemResource; + } + + /** + * Adding products to wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItems + * + * @return WishlistOutput + */ + public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutput + { + foreach ($wishlistItems as $wishlistItem) { + $this->updateItemInWishlist($wishlist, $wishlistItem); + } + + return $this->prepareOutput($wishlist); + } + + /** + * Update product item in wishlist + * + * @param Wishlist $wishlist + * @param WishlistItemData $wishlistItemData + * + * @return void + */ + private function updateItemInWishlist(Wishlist $wishlist, WishlistItemData $wishlistItemData): void + { + try { + $options = $this->buyRequestBuilder->build($wishlistItemData); + /** @var WishlistItem $wishlistItem */ + $wishlistItem = $this->wishlistItemFactory->create(); + $this->wishlistItemResource->load($wishlistItem, $wishlistItemData->getId()); + $wishlistItem->setDescription($wishlistItemData->getDescription()); + $resultItem = $wishlist->updateItem($wishlistItem, $options); + + if (is_string($resultItem)) { + $this->addError($resultItem); + } + } catch (LocalizedException $exception) { + $this->addError($exception->getMessage()); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/etc/graphql/di.xml b/app/code/Magento/Wishlist/etc/graphql/di.xml new file mode 100644 index 0000000000000..9726376bf30be --- /dev/null +++ b/app/code/Magento/Wishlist/etc/graphql/di.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="super_attribute" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\SuperAttributeDataProvider</item> + <item name="customizable_option" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\CustomizableOptionDataProvider</item> + <item name="bundle" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\BundleDataProvider</item> + <item name="downloadable" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\DownloadableLinkDataProvider</item> + <item name="grouped" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\SuperGroupDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php new file mode 100644 index 0000000000000..9cc1404613e41 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Mapper; + +use Magento\Wishlist\Model\Wishlist; + +/** + * Prepares the wishlist output as associative array + */ +class WishlistDataMapper +{ + /** + * Mapping the review data + * + * @param Wishlist $wishlist + * + * @return array + */ + public function map(Wishlist $wishlist): array + { + return [ + 'id' => $wishlist->getId(), + 'sharing_code' => $wishlist->getSharingCode(), + 'updated_at' => $wishlist->getUpdatedAt(), + 'items_count' => $wishlist->getItemsCount(), + 'name' => $wishlist->getName(), + 'model' => $wishlist, + ]; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php new file mode 100644 index 0000000000000..11c8446a72a9d --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\AddProductsToWishlist as AddProductsToWishlistModel; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Adding products to wishlist resolver + */ +class AddProductsToWishlist implements ResolverInterface +{ + /** + * @var AddProductsToWishlistModel + */ + private $addProductsToWishlist; + + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @param WishlistResourceModel $wishlistResource + * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig + * @param AddProductsToWishlistModel $addProductsToWishlist + * @param WishlistDataMapper $wishlistDataMapper + */ + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig, + AddProductsToWishlistModel $addProductsToWishlist, + WishlistDataMapper $wishlistDataMapper + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; + $this->addProductsToWishlist = $addProductsToWishlist; + $this->wishlistDataMapper = $wishlistDataMapper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + + $customerId = $context->getUserId(); + + /* Guest checking */ + if (null === $customerId || 0 === $customerId) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItems = $this->getWishlistItems($args['wishlistItems']); + $wishlistOutput = $this->addProductsToWishlist->execute($wishlist, $wishlistItems); + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'userInputErrors' => array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get wishlist items + * + * @param array $wishlistItemsData + * + * @return array + */ + private function getWishlistItems(array $wishlistItemsData): array + { + $wishlistItems = []; + + foreach ($wishlistItemsData as $wishlistItemData) { + $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData); + } + + return $wishlistItems; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if ($wishlistId !== null && $wishlistId > 0) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php index a84ce0e965b6d..cad574ef56ed2 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php @@ -9,9 +9,11 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; use Magento\Wishlist\Model\WishlistFactory; /** @@ -24,12 +26,21 @@ class CustomerWishlistResolver implements ResolverInterface */ private $wishlistFactory; + /** + * @var WishlistConfig + */ + private $wishlistConfig; + /** * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig */ - public function __construct(WishlistFactory $wishlistFactory) - { + public function __construct( + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig + ) { $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; } /** @@ -42,6 +53,10 @@ public function resolve( array $value = null, array $args = null ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + if (false === $context->getExtensionAttributes()->getIsCustomer()) { throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); } diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php new file mode 100644 index 0000000000000..1c741361ea7b7 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\Wishlist\RemoveProductsFromWishlist as RemoveProductsFromWishlistModel; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Removing products from wishlist resolver + */ +class RemoveProductsFromWishlist implements ResolverInterface +{ + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var RemoveProductsFromWishlistModel + */ + private $removeProductsFromWishlist; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @param WishlistFactory $wishlistFactory + * @param WishlistResourceModel $wishlistResource + * @param WishlistConfig $wishlistConfig + * @param WishlistDataMapper $wishlistDataMapper + * @param RemoveProductsFromWishlistModel $removeProductsFromWishlist + */ + public function __construct( + WishlistFactory $wishlistFactory, + WishlistResourceModel $wishlistResource, + WishlistConfig $wishlistConfig, + WishlistDataMapper $wishlistDataMapper, + RemoveProductsFromWishlistModel $removeProductsFromWishlist + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistConfig = $wishlistConfig; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistDataMapper = $wishlistDataMapper; + $this->removeProductsFromWishlist = $removeProductsFromWishlist; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + + $customerId = $context->getUserId(); + + /* Guest checking */ + if ($customerId === null || 0 === $customerId) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItemsIds = $args['wishlistItemsIds']; + $wishlistOutput = $this->removeProductsFromWishlist->execute($wishlist, $wishlistItemsIds); + + if (!empty($wishlistItemsIds)) { + $this->wishlistResource->save($wishlist); + } + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'userInputErrors' => \array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if ($wishlistId !== null && $wishlistId > 0) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php new file mode 100644 index 0000000000000..50a56863596c0 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\Wishlist\UpdateProductsInWishlist as UpdateProductsInWishlistModel; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Update wishlist items resolver + */ +class UpdateProductsInWishlist implements ResolverInterface +{ + /** + * @var UpdateProductsInWishlistModel + */ + private $updateProductsInWishlist; + + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @param WishlistResourceModel $wishlistResource + * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig + * @param UpdateProductsInWishlistModel $updateProductsInWishlist + * @param WishlistDataMapper $wishlistDataMapper + */ + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig, + UpdateProductsInWishlistModel $updateProductsInWishlist, + WishlistDataMapper $wishlistDataMapper + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; + $this->updateProductsInWishlist = $updateProductsInWishlist; + $this->wishlistDataMapper = $wishlistDataMapper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + + $customerId = $context->getUserId(); + + /* Guest checking */ + if (null === $customerId || $customerId === 0) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItems = $args['wishlistItems']; + $wishlistItems = $this->getWishlistItems($wishlistItems); + $wishlistOutput = $this->updateProductsInWishlist->execute($wishlist, $wishlistItems); + + if (count($wishlistOutput->getErrors()) !== count($wishlistItems)) { + $this->wishlistResource->save($wishlist); + } + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'userInputErrors' => \array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get DTO wishlist items + * + * @param array $wishlistItemsData + * + * @return array + */ + private function getWishlistItems(array $wishlistItemsData): array + { + $wishlistItems = []; + + foreach ($wishlistItemsData as $wishlistItemData) { + $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData); + } + + return $wishlistItems; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if (null !== $wishlistId && 0 < $wishlistId) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php index 792928ab61aaf..09c0a8a935a6c 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php @@ -8,10 +8,12 @@ namespace Magento\WishlistGraphQl\Model\Resolver; use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; use Magento\Wishlist\Model\WishlistFactory; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; @@ -30,14 +32,24 @@ class WishlistResolver implements ResolverInterface */ private $wishlistFactory; + /** + * @var WishlistConfig + */ + private $wishlistConfig; + /** * @param WishlistResourceModel $wishlistResource * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig */ - public function __construct(WishlistResourceModel $wishlistResource, WishlistFactory $wishlistFactory) - { + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig + ) { $this->wishlistResource = $wishlistResource; $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; } /** @@ -50,6 +62,10 @@ public function resolve( array $value = null, array $args = null ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + $customerId = $context->getUserId(); /* Guest checking */ diff --git a/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php b/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php index 8385d3ca852a4..017462b4c94c6 100644 --- a/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php +++ b/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php @@ -14,6 +14,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\GraphQl\Model\Query\ContextExtensionInterface; use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config; use Magento\Wishlist\Model\WishlistFactory; use Magento\WishlistGraphQl\Model\Resolver\CustomerWishlistResolver; use PHPUnit\Framework\MockObject\MockObject; @@ -48,6 +49,11 @@ class CustomerWishlistResolverTest extends TestCase */ private $resolver; + /** + * @var Config|MockObject + */ + private $wishlistConfigMock; + /** * Build the Testing Environment */ @@ -74,9 +80,12 @@ protected function setUp(): void ->setMethods(['loadByCustomerId', 'getId', 'getSharingCode', 'getUpdatedAt', 'getItemsCount']) ->getMock(); + $this->wishlistConfigMock = $this->createMock(Config::class); + $objectManager = new ObjectManager($this); $this->resolver = $objectManager->getObject(CustomerWishlistResolver::class, [ - 'wishlistFactory' => $this->wishlistFactoryMock + 'wishlistFactory' => $this->wishlistFactoryMock, + 'wishlistConfig' => $this->wishlistConfigMock ]); } @@ -85,6 +94,8 @@ protected function setUp(): void */ public function testThrowExceptionWhenUserNotAuthorized(): void { + $this->wishlistConfigMock->method('isEnabled')->willReturn(true); + // Given $this->extensionAttributesMock->method('getIsCustomer') ->willReturn(false); @@ -107,6 +118,8 @@ public function testThrowExceptionWhenUserNotAuthorized(): void */ public function testFactoryCreatesWishlistByAuthorizedCustomerId(): void { + $this->wishlistConfigMock->method('isEnabled')->willReturn(true); + // Given $this->extensionAttributesMock->method('getIsCustomer') ->willReturn(true); diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index deaa66921ba7c..794e90ed9f9a9 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -6,7 +6,7 @@ type Query { } type Customer { - wishlist: Wishlist! @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "The wishlist query returns the contents of a customer's wish lists") @cache(cacheable: false) + wishlist: Wishlist! @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "Contains the contents of a customer's wish lists") @cache(cacheable: false) } type WishlistOutput @doc(description: "Deprecated: `Wishlist` type should be used instead") { @@ -32,3 +32,45 @@ type WishlistItem { added_at: String @doc(description: "The time when the customer added the item to the wish list"), product: ProductInterface @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\ProductResolver") } + +type Mutation { + addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput @doc(description: "Adds one or more products to the specified wish list. This mutation supports all product types") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\AddProductsToWishlist") + removeProductsFromWishlist(wishlistId: ID!, wishlistItemsIds: [ID!]!): RemoveProductsFromWishlistOutput @doc(description: "Removes one or more products from the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\RemoveProductsFromWishlist") + updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput @doc(description: "Updates one or more products in the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\UpdateProductsInWishlist") +} + +input WishlistItemInput @doc(description: "Defines the items to add to a wish list") { + sku: String @doc(description: "The SKU of the product to add. For complex product types, specify the child product SKU") + quantity: Float @doc(description: "The amount or number of items to add") + parent_sku: String @doc(description: "For complex product types, the SKU of the parent product") + selected_options: [String!] @doc(description: "An array of strings corresponding to options the customer selected") + entered_options: [EnteredOptionInput!] @doc(description: "An array of options that the customer entered") +} + +type AddProductsToWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with all items that were successfully added") + userInputErrors:[CheckoutUserInputError]! @doc(description: "An array of errors encountered while adding products to a wish list") +} + +input EnteredOptionInput @doc(description: "Defines a customer-entered option") { + id: String! @doc(description: "A base64 encoded ID") + value: String! @doc(description: "Text the customer entered") +} + +type RemoveProductsFromWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with after items were successfully deleted") + userInputErrors:[CheckoutUserInputError]! @doc(description:"An array of errors encountered while deleting products from a wish list") +} + +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") + quantity: Float @doc(description: "The new amount or number of this item") + description: String @doc(description: "Describes the update") + selected_options: [String!] @doc(description: "An array of strings corresponding to options the customer selected") + entered_options: [EnteredOptionInput!] @doc(description: "An array of options that the customer entered") +} + +type UpdateProductsInWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with all items that were successfully updated") + userInputErrors:[CheckoutUserInputError]! @doc(description:"An array of errors encountered while updating products in a wish list") +} diff --git a/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js b/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js index 87632a6962cc5..cae30c83d95bc 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js +++ b/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js @@ -5,7 +5,6 @@ var config = { deps: [ - 'Magento_Theme/js/responsive', 'Magento_Theme/js/theme' ] }; diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js b/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js deleted file mode 100644 index 011417f54ad9a..0000000000000 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([ - 'jquery', - 'matchMedia', - 'mage/tabs', - 'domReady!' -], function ($, mediaCheck) { - 'use strict'; - - mediaCheck({ - media: '(min-width: 768px)', - - /** - * Switch to Desktop Version. - */ - entry: function () { - var galleryElement; - - (function () { - - var productInfoMain = $('.product-info-main'), - productInfoAdditional = $('#product-info-additional'); - - if (productInfoAdditional.length) { - productInfoAdditional.addClass('hidden'); - productInfoMain.removeClass('responsive'); - } - - })(); - - galleryElement = $('[data-role=media-gallery]'); - - if (galleryElement.length && galleryElement.data('mageZoom')) { - galleryElement.zoom('enable'); - } - - if (galleryElement.length && galleryElement.data('mageGallery')) { - galleryElement.gallery('option', 'disableLinks', true); - galleryElement.gallery('option', 'showNav', false); - galleryElement.gallery('option', 'showThumbs', true); - } - }, - - /** - * Switch to Mobile Version. - */ - exit: function () { - var galleryElement; - - $('.action.toggle.checkout.progress').on('click.gotoCheckoutProgress', function () { - var myWrapper = '#checkout-progress-wrapper'; - - scrollTo(myWrapper + ' .title'); - $(myWrapper + ' .title').addClass('active'); - $(myWrapper + ' .content').show(); - }); - - $('body').on('click.checkoutProgress', '#checkout-progress-wrapper .title', function () { - $(this).toggleClass('active'); - $('#checkout-progress-wrapper .content').toggle(); - }); - - galleryElement = $('[data-role=media-gallery]'); - - setTimeout(function () { - if (galleryElement.length && galleryElement.data('mageZoom')) { - galleryElement.zoom('disable'); - } - - if (galleryElement.length && galleryElement.data('mageGallery')) { - galleryElement.gallery('option', 'disableLinks', false); - galleryElement.gallery('option', 'showNav', true); - galleryElement.gallery('option', 'showThumbs', false); - } - }, 2000); - } - }); -}); diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js b/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js index ab8a6063f29a7..e4edd3bd8662c 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js +++ b/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js @@ -12,21 +12,9 @@ define([ ], function ($, keyboardHandler) { 'use strict'; - if ($('body').hasClass('checkout-cart-index')) { - if ($('#co-shipping-method-form .fieldset.rates').length > 0 && - $('#co-shipping-method-form .fieldset.rates :checked').length === 0 - ) { - $('#block-shipping').on('collapsiblecreate', function () { - $('#block-shipping').collapsible('forceActivate'); - }); - } - } - $('.cart-summary').mage('sticky', { container: '#maincontent' }); - $('.panel.header > .header.links').clone().appendTo('#store\\.links'); - keyboardHandler.apply(); }); diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php new file mode 100644 index 0000000000000..9c7abcd6c8364 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php @@ -0,0 +1,213 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Api; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Registry; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Integration\Model\Oauth\Token as TokenModel; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Customer as CustomerHelper; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php + */ +class CustomerSharingOptionsTest extends WebapiAbstract +{ + const RESOURCE_PATH = '/V1/customers/me'; + const REPO_SERVICE = 'customerCustomerRepositoryV1'; + const SERVICE_VERSION = 'V1'; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @var CustomerHelper + */ + private $customerHelper; + + /** + * @var TokenModel + */ + private $token; + + /** + * @var CustomerInterface + */ + private $customerData; + + /** + * @var CustomerTokenServiceInterface + */ + private $tokenService; + + /** + * Execute per test initialization. + */ + public function setUp(): void + { + $this->customerRegistry = Bootstrap::getObjectManager()->get( + \Magento\Customer\Model\CustomerRegistry::class + ); + + $this->customerRepository = Bootstrap::getObjectManager()->get( + CustomerRepositoryInterface::class, + ['customerRegistry' => $this->customerRegistry] + ); + + $this->customerHelper = new CustomerHelper(); + $this->customerData = $this->customerHelper->createSampleCustomer(); + $this->tokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + + // get token + $this->resetTokenForCustomerSampleData(); + } + + /** + * Ensure that fixture customer and his addresses are deleted. + */ + public function tearDown(): void + { + $this->customerRepository = null; + + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + parent::tearDown(); + } + + /** + * @param string $storeCode + * @param bool $expectingException + * @dataProvider getCustomerDataWebsiteScopeDataProvider + * + * @magentoConfigFixture default_store customer/account_share/scope 1 + */ + public function testGetCustomerDataWebsiteScope(string $storeCode, bool $expectingException) + { + $this->_markTestAsRestOnly('SOAP is difficult to generate exception messages, inconsistencies in WSDL'); + $this->processGetCustomerData($storeCode, $expectingException); + } + + /** + * @param string $storeCode + * @param bool $expectingException + * @dataProvider getCustomerDataGlobalScopeDataProvider + * + * @magentoConfigFixture customer/account_share/scope 0 + */ + public function testGetCustomerDataGlobalScope(string $storeCode, bool $expectingException) + { + $this->processGetCustomerData($storeCode, $expectingException); + } + + /** + * @param string $storeCode + * @param bool $expectingException + */ + private function processGetCustomerData(string $storeCode, bool $expectingException) + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_GET, + 'token' => $this->token, + ], + 'soap' => [ + 'service' => self::REPO_SERVICE, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::REPO_SERVICE . 'GetSelf', + 'token' => $this->token + ] + ]; + $arguments = []; + if (TESTS_WEB_API_ADAPTER === 'soap') { + $arguments['customerId'] = 0; + } + + if ($expectingException) { + self::expectException(\Exception::class); + self::expectExceptionMessage("The consumer isn't authorized to access %resources."); + } + + $this->_webApiCall($serviceInfo, $arguments, null, $storeCode); + } + + /** + * Data provider for testGetCustomerDataWebsiteScope. + * + * @return array + */ + public function getCustomerDataWebsiteScopeDataProvider(): array + { + return [ + 'Default Store View' => [ + 'store_code' => 'default', + 'exception' => false + ], + 'Custom Store View' => [ + 'store_code' => 'fixture_second_store', + 'exception' => true + ] + ]; + } + + /** + * Data provider for testGetCustomerDataGlobalScope. + * + * @return array + */ + public function getCustomerDataGlobalScopeDataProvider(): array + { + return [ + 'Default Store View' => [ + 'store_code' => 'default', + 'exception' => false + ], + 'Custom Store View' => [ + 'store_code' => 'fixture_second_store', + 'exception' => false + ] + ]; + } + + /** + * Sets the test's access token for the created customer sample data + */ + private function resetTokenForCustomerSampleData() + { + $this->resetTokenForCustomer($this->customerData[CustomerInterface::EMAIL], 'test@123'); + } + + /** + * Sets the test's access token for a particular username and password. + * + * @param string $username + * @param string $password + */ + private function resetTokenForCustomer($username, $password) + { + $this->token = $this->tokenService->createCustomerAccessToken($username, $password); + $this->customerRegistry->remove($this->customerRepository->get($username)->getId()); + } +} 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 00eb235cb4dc3..d6477c82513e9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -714,4 +714,60 @@ private function assertCategoryChildren(array $category, array $expectedChildren $this->assertResponseFields($category['children'][$i], $expectedChild); } } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterCategoryInlineFragment() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {eq: "6"}}){ + ... on CategoryTree { + id + name + url_key + url_path + children_count + path + position + } + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterCategoryNamedFragment() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {eq: "6"}}){ + ...Cat + } +} + +fragment Cat on CategoryTree { + id + name + url_key + url_path + children_count + path + position +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); + } } 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 1a95a3d6f4925..dd5b5827c8017 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -570,8 +570,8 @@ public function testSearchAndFilterByCustomAttribute() ], [ 'count' => 1, - 'label' => '40-*', - 'value' => '40_*', + 'label' => '40-50', + 'value' => '40_50', ], ], @@ -1431,8 +1431,8 @@ public function testFilterProductsForExactMatchingName() 'count' => 1, ], [ - 'label' => '20-*', - 'value' => '20_*', + 'label' => '20-30', + 'value' => '20_30', 'count' => 1, ] ] 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 99fdfb2cf1b00..c6719f1862ddc 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -665,7 +665,8 @@ private function assertMediaGalleryEntries($product, $actualResponse) { $mediaGalleryEntries = $product->getMediaGalleryEntries(); $this->assertCount(1, $mediaGalleryEntries, "Precondition failed, incorrect number of media gallery entries."); - $this->assertIsArray([$actualResponse['media_gallery_entries']], + $this->assertIsArray( + [$actualResponse['media_gallery_entries']], "Media galleries field must be of an array type." ); $this->assertCount(1, $actualResponse['media_gallery_entries'], "There must be 1 record in media gallery."); @@ -701,10 +702,10 @@ private function assertMediaGalleryEntries($product, $actualResponse) */ private function assertCustomAttribute($actualResponse) { - $customAttribute = null; + $customAttribute = 'customAttributeValue'; $this->assertEquals($customAttribute, $actualResponse['attribute_code_custom']); } - + /** * @param ProductInterface $product * @param $actualResponse diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php new file mode 100644 index 0000000000000..25c808a549e80 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl; + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\GraphQl\Model\Cors\Configuration; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class CorsHeadersTest extends GraphQlAbstract +{ + /** + * @var Config $config + */ + private $resourceConfig; + /** + * @var ReinitableConfigInterface + */ + private $reinitConfig; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $objectManager = ObjectManager::getInstance(); + + $this->resourceConfig = $objectManager->get(Config::class); + $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0); + $this->reinitConfig->reinit(); + } + + public function testNoCorsHeadersWhenCorsIsDisabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, 'Origin'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '1'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, 'GET,POST'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'magento.local'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400'); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertArrayNotHasKey('Access-Control-Max-Age', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers); + } + + public function testCorsHeadersWhenCorsIsEnabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, 'Origin'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '1'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, 'GET,POST'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'magento.local'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400'); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertEquals('Origin', $headers['Access-Control-Allow-Headers']); + self::assertEquals('1', $headers['Access-Control-Allow-Credentials']); + self::assertEquals('GET,POST', $headers['Access-Control-Allow-Methods']); + self::assertEquals('magento.local', $headers['Access-Control-Allow-Origin']); + self::assertEquals('86400', $headers['Access-Control-Max-Age']); + } + + public function testEmptyCorsHeadersWhenCorsIsEnabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, ''); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertArrayNotHasKey('Access-Control-Max-Age', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers); + } + + private function getHeadersFromIntrospectionQuery(): array + { + $query + = <<<QUERY + query IntrospectionQuery { + __schema { + types { + name + } + } + } +QUERY; + + return $this->graphQlQueryWithResponseHeaders($query)['headers'] ?? []; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php index c42450d86fd58..8d6bae35de49b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php @@ -368,6 +368,32 @@ public function testEmptyCustomerLastName() $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerWithIncorrectGender() + { + $gender = 5; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('"' . $gender . '" is not a valid gender value.'); + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + gender: {$gender} + } + ) { + customer { + gender + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); + } + /** * @magentoApiDataFixture Magento/Customer/_files/customer.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php new file mode 100644 index 0000000000000..fa0909d556b3a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\GiftMessage\Cart\Item; + +use Exception; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class GiftMessageTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 0 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageCartForItemNotAllow() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_guest_order_with_gift_message'); + foreach ($this->requestCartResult($maskedQuoteId)['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertNull($item['gift_message']); + } + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 1 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageCartForItem() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_guest_order_with_gift_message'); + foreach ($this->requestCartResult($maskedQuoteId)['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertArrayHasKey('to', $item['gift_message']); + self::assertArrayHasKey('from', $item['gift_message']); + self::assertArrayHasKey('message', $item['gift_message']); + } + } + + /** + * @param string $quoteId + * + * @return array|bool|float|int|string + * @throws Exception + */ + private function requestCartResult(string $quoteId) + { + $query = <<<QUERY +{ + cart(cart_id: "$quoteId") { + items { + product { + name + } + ... on SimpleCartItem { + gift_message { + to + from + message + } + } + } + } +} +QUERY; + return $this->graphQlQuery($query); + } +} 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 e6db0b9e808ef..8cb0a6db972b4 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php @@ -7,12 +7,29 @@ namespace Magento\GraphQl\GroupedProduct; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; +/** + * Class to test GraphQl response with grouped products + */ class GroupedProductViewTest extends GraphQlAbstract { + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + } /** * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped.php @@ -20,17 +37,16 @@ class GroupedProductViewTest extends GraphQlAbstract public function testAllFieldsGroupedProduct() { $productSku = 'grouped-product'; - $query - = <<<QUERY + $query = <<<QUERY { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { id attribute_set_id created_at name sku - type_id + type_id ... on GroupedProduct { items{ qty @@ -39,9 +55,14 @@ public function testAllFieldsGroupedProduct() sku name type_id - url_key + url_key } } + product_links{ + linked_product_sku + position + link_type + } } } } @@ -49,47 +70,77 @@ public function testAllFieldsGroupedProduct() QUERY; $response = $this->graphQlQuery($query); - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $groupedProduct = $productRepository->get($productSku, false, null, true); + $groupedProduct = $this->productRepository->get($productSku, false, null, true); - $this->assertGroupedProductItems($groupedProduct, $response['products']['items'][0]); + $this->assertNotEmpty( + $response['products']['items'][0]['items'], + "Precondition failed: 'Grouped product items' must not be empty" + ); + $this->assertGroupedProductItems($groupedProduct, $response['products']['items'][0]['items']); + $this->assertNotEmpty( + $response['products']['items'][0]['product_links'], + "Precondition failed: 'Linked product items' must not be empty" + ); + $this->assertProductLinks($groupedProduct, $response['products']['items'][0]['product_links']); } - private function assertGroupedProductItems($product, $actualResponse) + /** + * @param ProductInterface $product + * @param array $items + */ + private function assertGroupedProductItems(ProductInterface $product, array $items): void { - $this->assertNotEmpty( - $actualResponse['items'], - "Precondition failed: 'grouped product items' must not be empty" - ); - $this->assertCount(2, $actualResponse['items']); + $this->assertCount(2, $items); $groupedProductLinks = $product->getProductLinks(); - foreach ($actualResponse['items'] as $itemIndex => $bundleItems) { - $this->assertNotEmpty($bundleItems); + foreach ($items as $itemIndex => $bundleItem) { + $this->assertNotEmpty($bundleItem); $associatedProductSku = $groupedProductLinks[$itemIndex]->getLinkedProductSku(); - - $productsRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $associatedProduct */ - $associatedProduct = $productsRepository->get($associatedProductSku); + $associatedProduct = $this->productRepository->get($associatedProductSku); $this->assertEquals( $groupedProductLinks[$itemIndex]->getExtensionAttributes()->getQty(), - $actualResponse['items'][$itemIndex]['qty'] + $bundleItem['qty'] ); $this->assertEquals( $groupedProductLinks[$itemIndex]->getPosition(), - $actualResponse['items'][$itemIndex]['position'] + $bundleItem['position'] ); $this->assertResponseFields( - $actualResponse['items'][$itemIndex]['product'], + $bundleItem['product'], [ - 'sku' => $associatedProductSku, - 'type_id' => $groupedProductLinks[$itemIndex]->getLinkedProductType(), - 'url_key'=> $associatedProduct->getUrlKey(), - 'name' => $associatedProduct->getName() + 'sku' => $associatedProductSku, + 'type_id' => $groupedProductLinks[$itemIndex]->getLinkedProductType(), + 'url_key'=> $associatedProduct->getUrlKey(), + 'name' => $associatedProduct->getName() ] ); } } + + /** + * @param ProductInterface $product + * @param array $links + * @return void + */ + private function assertProductLinks(ProductInterface $product, array $links): void + { + $this->assertCount(2, $links); + $productLinks = $product->getProductLinks(); + foreach ($links as $itemIndex => $linkedItem) { + $this->assertNotEmpty($linkedItem); + $this->assertEquals( + $productLinks[$itemIndex]->getPosition(), + $linkedItem['position'] + ); + $this->assertEquals( + $productLinks[$itemIndex]->getLinkedProductSku(), + $linkedItem['linked_product_sku'] + ); + $this->assertEquals( + $productLinks[$itemIndex]->getLinkType(), + $linkedItem['link_type'] + ); + } + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php index 21a8d6ae94312..ff8d4f4280c10 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php @@ -281,6 +281,50 @@ public function testSetDisabledPaymentOnCart() $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWitMissingCartId() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = ""; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"cart_id\" is missing" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithMissingPaymentMethod() + { + $methodCode = ""; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"code\" for \"payment_method\" is missing." + ); + $this->graphQlMutation($query); + } + /** * @param string $maskedQuoteId * @param string $methodCode diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php index 3e06b89c77fb7..900a2877e8c7b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php @@ -1745,6 +1745,57 @@ public function testSetNewShippingAddressWithDefaultValueOfSaveInAddressBookAndP } } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetShippingAddressOnCartWithNullCustomerAddressId() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: null + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + label + code + } + region { + code + label + } + __typename + } + } + } +} +QUERY; + $this->expectExceptionMessage( + 'The shipping address must contain either "customer_address_id" or "address".' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + /** * Verify the all the whitelisted fields for a New Address Object * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php index dbc10700794fa..78691d8cbd889 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php @@ -218,6 +218,50 @@ public function testSetDisabledPaymentOnCart() $this->graphQlMutation($query); } + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWitMissingCartId() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = ""; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"cart_id\" is missing" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithMissingPaymentMethod() + { + $methodCode = ""; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"code\" for \"payment_method\" is missing." + ); + $this->graphQlMutation($query); + } + /** * @param string $maskedQuoteId * @param string $methodCode diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php index a17bc1aa3821a..0a22f3ca9721c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php @@ -7,8 +7,9 @@ namespace Magento\GraphQl\Quote\Guest; -use Magento\Quote\Model\QuoteFactory; +use Exception; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; @@ -273,6 +274,81 @@ private function getCartQuery(string $maskedQuoteId) } } } +QUERY; + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 0 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws Exception + */ + public function testUpdateGiftMessageCartForItemNotAllow() + { + $query = $this->getUpdateGiftMessageQuery(); + foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) { + self::assertNull($item['gift_message']); + } + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 1 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws Exception + */ + public function testUpdateGiftMessageCartForItem() + { + $query = $this->getUpdateGiftMessageQuery(); + foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertSame('Alex', $item['gift_message']['to']); + self::assertSame('Mike', $item['gift_message']['from']); + self::assertSame('Best regards.', $item['gift_message']['message']); + } + } + + private function getUpdateGiftMessageQuery() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_guest_order_with_gift_message', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $itemId = (int)$quote->getItemByProduct($this->productRepository->get('simple'))->getId(); + + return <<<QUERY +mutation { + updateCartItems( + input: { + cart_id: "$maskedQuoteId", + cart_items: [ + { + cart_item_id: $itemId + quantity: 3 + gift_message: { + to: "Alex" + from: "Mike" + message: "Best regards." + } + } + ] + } + ) { + cart { + items { + id + product { + name + } + quantity + ... on SimpleCartItem { + gift_message { + to + from + message + } + } + } + } + } +} QUERY; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php index 61834ea71c24f..ed7cda3b46ebc 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php @@ -157,7 +157,10 @@ public function testCustomerOrdersSimpleProductWithTaxesAndDiscounts() // Asserting discounts on order item level $this->assertEquals(4, $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['value']); $this->assertEquals('USD', $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['currency']); - $this->assertEquals('Discount', $customerOrderResponse[0]['items'][0]['discounts'][0]['label']); + $this->assertEquals( + 'Discount Label for 10% off', + $customerOrderResponse[0]['items'][0]['discounts'][0]['label'] + ); $customerOrderItem = $customerOrderResponse[0]; $this->assertTotalsWithTaxesAndDiscounts($customerOrderItem['total']); $this->deleteOrder(); @@ -168,20 +171,13 @@ public function testCustomerOrdersSimpleProductWithTaxesAndDiscounts() */ private function assertTotalsWithTaxesAndDiscounts(array $customerOrderItemTotal): void { - $this->assertCount(2, $customerOrderItemTotal['taxes']); - $expectedProductAndShippingTaxes = [2.7, 1.35]; - $totalTaxes = []; - foreach ($customerOrderItemTotal['taxes'] as $totalTaxFromResponse) { - array_push($totalTaxes, $totalTaxFromResponse['amount']['value']); - } - foreach ($totalTaxes as $value) { - $this->assertTrue(in_array($value, $expectedProductAndShippingTaxes)); - } - foreach ($customerOrderItemTotal['taxes'] as $taxData) { - $this->assertEquals('USD', $taxData['amount']['currency']); - $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); - $this->assertEquals(7.5, $taxData['rate']); - } + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(4.05, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + unset($customerOrderItemTotal['taxes']); $assertionMap = [ 'base_grand_total' => ['value' => 58.05, 'currency' =>'USD'], @@ -195,7 +191,92 @@ private function assertTotalsWithTaxesAndDiscounts(array $customerOrderItemTotal 'total_amount' => ['value' => 20, 'currency' =>'USD'], 'discounts' => [ 0 => ['amount'=>['value'=> 2, 'currency' =>'USD'], - 'label' => 'Discount' + 'label' => 'Discount Label for 10% off' + ] + ], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 1.35], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ] + ], + 'discounts' => [ + 0 => ['amount' => [ 'value' => 6, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Verify the customer order with tax, discount with shipping tax class set for calculation setting + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testCustomerOrdersSimpleProductWithTaxesAndDiscountsWithTwoRules() + { + $quantity = 4; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + // Asserting discounts on order item level + $this->assertEquals(4, $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['currency']); + $this->assertEquals( + 'Discount Label for 10% off', + $customerOrderResponse[0]['items'][0]['discounts'][0]['label'] + ); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsWithTaxesAndDiscountsWithTwoRules($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * @param array $customerOrderItemTotal + */ + private function assertTotalsWithTaxesAndDiscountsWithTwoRules(array $customerOrderItemTotal): void + { + $this->assertCount(2, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(4.05, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + $secondTaxData = $customerOrderItemTotal['taxes'][1]; + $this->assertEquals('USD', $secondTaxData['amount']['currency']); + $this->assertEquals(2.97, $secondTaxData['amount']['value']); + $this->assertEquals('US-AL-*-Rate-1', $secondTaxData['title']); + $this->assertEquals(5.5, $secondTaxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 61.02, 'currency' =>'USD'], + 'grand_total' => ['value' => 61.02, 'currency' =>'USD'], + 'subtotal' => ['value' => 40, 'currency' =>'USD'], + 'total_tax' => ['value' => 7.02, 'currency' =>'USD'], + 'total_shipping' => ['value' => 20, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 22.6], + 'amount_excluding_tax' => ['value' => 20], + 'total_amount' => ['value' => 20, 'currency' =>'USD'], + 'discounts' => [ + 0 => ['amount'=>['value'=> 2, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' ] ], 'taxes'=> [ @@ -203,12 +284,17 @@ private function assertTotalsWithTaxesAndDiscounts(array $customerOrderItemTotal 'amount'=>['value' => 1.35], 'title' => 'US-TEST-*-Rate-1', 'rate' => 7.5 + ], + 1 => [ + 'amount'=>['value' => 0.99], + 'title' => 'US-AL-*-Rate-1', + 'rate' => 5.5 ] ] ], 'discounts' => [ - 0 => ['amount' => [ 'value' => -6, 'currency' =>'USD'], - 'label' => 'Discount' + 0 => ['amount' => [ 'value' => 6, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' ] ] ]; @@ -744,21 +830,13 @@ public function testCustomerOrderWithTaxesExcludedOnShipping() */ private function assertTotalsAndShippingWithExcludedTaxSetting($customerOrderItemTotal): void { - $this->assertCount(2, $customerOrderItemTotal['taxes']); - $expectedProductAndShippingTaxes = [1.5, 0.75]; + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(2.25, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); - $totalTaxes = []; - foreach ($customerOrderItemTotal['taxes'] as $totalTaxFromResponse) { - array_push($totalTaxes, $totalTaxFromResponse['amount']['value']); - } - foreach ($totalTaxes as $value) { - $this->assertTrue(in_array($value, $expectedProductAndShippingTaxes)); - } - foreach ($customerOrderItemTotal['taxes'] as $taxData) { - $this->assertEquals('USD', $taxData['amount']['currency']); - $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); - $this->assertEquals(7.5, $taxData['rate']); - } unset($customerOrderItemTotal['taxes']); $assertionMap = [ 'base_grand_total' => ['value' => 32.25, 'currency' =>'USD'], @@ -819,20 +897,14 @@ public function testCustomerOrderWithTaxesIncludedOnShippingAndTotals() */ private function assertTotalsAndShippingWithTaxes(array $customerOrderItemTotal): void { - $this->assertCount(2, $customerOrderItemTotal['taxes']); - $expectedProductAndShippingTaxes = [1.5, 0.75]; - $totalTaxes = []; - foreach ($customerOrderItemTotal['taxes'] as $totalTaxFromResponse) { - array_push($totalTaxes, $totalTaxFromResponse['amount']['value']); - } - foreach ($totalTaxes as $value) { - $this->assertTrue(in_array($value, $expectedProductAndShippingTaxes)); - } - foreach ($customerOrderItemTotal['taxes'] as $taxData) { - $this->assertEquals('USD', $taxData['amount']['currency']); - $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); - $this->assertEquals(7.5, $taxData['rate']); - } + $this->assertCount(1, $customerOrderItemTotal['taxes']); + + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(2.25, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + unset($customerOrderItemTotal['taxes']); unset($customerOrderItemTotal['shipping_handling']['discounts']); $assertionMap = [ @@ -845,7 +917,6 @@ private function assertTotalsAndShippingWithTaxes(array $customerOrderItemTotal) 'amount_including_tax' => ['value' => 10.75], 'amount_excluding_tax' => ['value' => 10], 'total_amount' => ['value' => 10, 'currency' =>'USD'], - 'taxes'=> [ 0 => [ 'amount'=>['value' => 0.75], diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php index 7db72ce6dfcd3..f0a63b10b2a5b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php @@ -145,6 +145,11 @@ public function testGetCustomerOrderBundleProductWithTaxesAndDiscounts() 'bundle-product-two-dropdown-options-simple1-simple2', $bundledItemInTheOrder['product_sku'] ); + $this->assertEquals(6, $bundledItemInTheOrder['discounts'][0]['amount']['value']); + $this->assertEquals( + 'Discount Label for 10% off', + $bundledItemInTheOrder["discounts"][0]['label'] + ); $this->assertArrayHasKey('bundle_options', $bundledItemInTheOrder); $childItemsInTheOrder = $bundledItemInTheOrder['bundle_options']; $this->assertNotEmpty($childItemsInTheOrder); @@ -163,20 +168,13 @@ public function testGetCustomerOrderBundleProductWithTaxesAndDiscounts() */ private function assertTotalsOnBundleProductWithTaxesAndDiscounts(array $customerOrderItemTotal): void { - $this->assertCount(2, $customerOrderItemTotal['taxes']); - $expectedProductAndShippingTaxes = [4.05, 1.35]; - $totalTaxes = []; - foreach ($customerOrderItemTotal['taxes'] as $totalTaxFromResponse) { - array_push($totalTaxes, $totalTaxFromResponse['amount']['value']); - } - foreach ($totalTaxes as $value) { - $this->assertTrue(in_array($value, $expectedProductAndShippingTaxes)); - } - foreach ($customerOrderItemTotal['taxes'] as $taxData) { - $this->assertEquals('USD', $taxData['amount']['currency']); - $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); - $this->assertEquals(7.5, $taxData['rate']); - } + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(5.4, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + unset($customerOrderItemTotal['taxes']); $assertionMap = [ 'base_grand_total' => ['value' => 77.4, 'currency' =>'USD'], @@ -189,8 +187,8 @@ private function assertTotalsOnBundleProductWithTaxesAndDiscounts(array $custome 'amount_excluding_tax' => ['value' => 20], 'total_amount' => ['value' => 20], 'discounts' => [ - 0 => ['amount'=>['value'=>2], - 'label' => 'Discount' + 0 => ['amount'=>['value'=> 2], + 'label' => 'Discount Label for 10% off' ] ], 'taxes'=> [ @@ -202,8 +200,8 @@ private function assertTotalsOnBundleProductWithTaxesAndDiscounts(array $custome ] ], 'discounts' => [ - 0 => ['amount' => [ 'value' => -8, 'currency' =>'USD'], - 'label' => 'Discount' + 0 => ['amount' => [ 'value' => 8, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' ] ] ]; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php index 48619d1392309..e7af77f23fa88 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php @@ -91,6 +91,6 @@ public function testGetStoreConfig() $response['storeConfig']['secure_base_static_url'] ); $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $response['storeConfig']['secure_base_media_url']); - $this->assertEquals('Test Store', $response['storeConfig']['store_name']); + $this->assertEquals($store->getName(), $response['storeConfig']['store_name']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php new file mode 100644 index 0000000000000..c0199e8908d0e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php @@ -0,0 +1,176 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\Product\Type; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\WishlistFactory; + +/** + * Test coverage for adding a bundle product to wishlist + */ +class AddBundleProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var mixed + */ + private $productRepository; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $objectManager->get(WishlistFactory::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/product_1.php + * + * @throws Exception + */ + public function testAddBundleProductWithOptions(): void + { + $sku = 'bundle-product'; + $product = $this->productRepository->get($sku); + $customerId = 1; + $qty = 2; + $optionQty = 1; + + /** @var Type $typeInstance */ + $typeInstance = $product->getTypeInstance(); + $typeInstance->setStoreFilter($product->getStoreId(), $product); + /** @var Option $option */ + $option = $typeInstance->getOptionsCollection($product)->getFirstItem(); + /** @var Product $selection */ + $selection = $typeInstance->getSelectionsCollection([$option->getId()], $product)->getFirstItem(); + $optionId = $option->getId(); + $selectionId = $selection->getSelectionId(); + $bundleOptions = $this->generateBundleOptionIdV2((int) $optionId, (int) $selectionId, $optionQty); + + $query = $this->getQuery($sku, $qty, $bundleOptions); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $wishlist = $this->wishlistFactory->create()->loadByCustomerId($customerId, true); + /** @var Item $item */ + $item = $wishlist->getItemCollection()->getFirstItem(); + + $this->assertArrayHasKey('addProductsToWishlist', $response); + $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $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'][0]['qty']); + $this->assertEquals($item->getDescription(), $response['items'][0]['description']); + $this->assertEquals($item->getAddedAt(), $response['items'][0]['added_at']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param string $sku + * @param int $qty + * @param string $bundleOptions + * @param int $wishlistId + * + * @return string + */ + private function getQuery( + string $sku, + int $qty, + string $bundleOptions, + int $wishlistId = 0 + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + sku: "{$sku}" + quantity: {$qty} + selected_options: [ + "{$bundleOptions}" + ] + } + ] +) { + userInputErrors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items { + id + description + qty + added_at + } + } + } +} +MUTATION; + } + + /** + * @param int $optionId + * @param int $selectionId + * + * @param int $quantity + * + * @return string + */ + private function generateBundleOptionIdV2(int $optionId, int $selectionId, int $quantity): string + { + return base64_encode("bundle/$optionId/$selectionId/$quantity"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php new file mode 100644 index 0000000000000..386df99f0d211 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\WishlistFactory; + +/** + * Test coverage for adding a configurable product to wishlist + */ +class AddConfigurableProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $objectManager->get(WishlistFactory::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * + * @throws Exception + */ + public function testAddDownloadableProductWithOptions(): void + { + $product = $this->getConfigurableProductInfo(); + $customerId = 1; + $qty = 2; + $attributeId = (int) $product['configurable_options'][0]['attribute_id']; + $valueIndex = $product['configurable_options'][0]['values'][0]['value_index']; + $childSku = $product['variants'][0]['product']['sku']; + $parentSku = $product['sku']; + $selectedConfigurableOptionsQuery = $this->generateSuperAttributesIdV2Query($attributeId, $valueIndex); + + $query = $this->getQuery($parentSku, $childSku, $qty, $selectedConfigurableOptionsQuery); + + $response = $this->graphQlMutation($query, [], '', $this->getHeadersMap()); + $wishlist = $this->wishlistFactory->create()->loadByCustomerId($customerId, true); + /** @var Item $wishlistItem */ + $wishlistItem = $wishlist->getItemCollection()->getFirstItem(); + + self::assertArrayHasKey('addProductsToWishlist', $response); + self::assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $wishlistResponse = $response['addProductsToWishlist']['wishlist']; + self::assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); + self::assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); + self::assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); + self::assertEquals($wishlistItem->getId(), $wishlistResponse['items'][0]['id']); + self::assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items'][0]['qty']); + self::assertEquals($wishlistItem->getDescription(), $wishlistResponse['items'][0]['description']); + self::assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items'][0]['added_at']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeadersMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param string $parentSku + * @param string $childSku + * @param int $qty + * @param string $customizableOptions + * @param int $wishlistId + * + * @return string + */ + private function getQuery( + string $parentSku, + string $childSku, + int $qty, + string $customizableOptions, + int $wishlistId = 0 + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + sku: "{$childSku}" + parent_sku: "{$parentSku}" + quantity: {$qty} + {$customizableOptions} + } + ] +) { + userInputErrors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items { + id + description + qty + added_at + } + } + } +} +MUTATION; + } + + /** + * Generates Id_v2 for super configurable product super attributes + * + * @param int $attributeId + * @param int $valueIndex + * + * @return string + */ + private function generateSuperAttributesIdV2Query(int $attributeId, int $valueIndex): string + { + return 'selected_options: ["' . base64_encode("configurable/$attributeId/$valueIndex") . '"]'; + } + + /** + * Returns information about testable configurable product retrieved from GraphQl query + * + * @return array + * + * @throws Exception + */ + private function getConfigurableProductInfo(): array + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable')); + + return current($searchResponse['products']['items']); + } + + /** + * Returns GraphQl query for fetching configurable product information + * + * @param string $term + * + * @return string + */ + private function getFetchProductQuery(string $term): string + { + return <<<QUERY +{ + products( + search:"{$term}" + pageSize:1 + ) { + items { + sku + ... on ConfigurableProduct { + variants { + product { + sku + } + } + configurable_options { + attribute_id + attribute_code + id + label + position + product_id + use_default + values { + default_label + label + store_label + use_default_value + value_index + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php new file mode 100644 index 0000000000000..389f4eae4c574 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php @@ -0,0 +1,216 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\WishlistFactory; + +/** + * Test coverage for adding a downloadable product to wishlist + */ +class AddDownloadableProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var GetCustomOptionsWithIDV2ForQueryBySku + */ + private $getCustomOptionsWithIDV2ForQueryBySku; + + /** + * Set Up + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $this->objectManager->get(WishlistFactory::class); + $this->getCustomOptionsWithIDV2ForQueryBySku = + $this->objectManager->get(GetCustomOptionsWithIDV2ForQueryBySku::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php + */ + public function testAddDownloadableProductWithOptions(): void + { + $customerId = 1; + $sku = 'downloadable-product-with-purchased-separately-links'; + $qty = 2; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); + $productOptionsQuery = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ); + $query = $this->getQuery($qty, $sku, trim($productOptionsQuery, '{}')); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $wishlist = $this->wishlistFactory->create(); + $wishlist->loadByCustomerId($customerId, true); + /** @var Item $wishlistItem */ + $wishlistItem = $wishlist->getItemCollection()->getFirstItem(); + + self::assertArrayHasKey('addProductsToWishlist', $response); + self::assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $wishlistResponse = $response['addProductsToWishlist']['wishlist']; + self::assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); + self::assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); + self::assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); + self::assertEquals($wishlistItem->getId(), $wishlistResponse['items'][0]['id']); + self::assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items'][0]['qty']); + self::assertEquals($wishlistItem->getDescription(), $wishlistResponse['items'][0]['description']); + self::assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items'][0]['added_at']); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 0 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php + */ + public function testAddDownloadableProductOnDisabledWishlist(): void + { + $qty = 2; + $sku = 'downloadable-product-with-purchased-separately-links'; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); + $productOptionsQuery = trim(preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ), '{}'); + $query = $this->getQuery($qty, $sku, $productOptionsQuery); + $this->expectExceptionMessage('The wishlist is not currently available.'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Function returns array of all product's links + * + * @param string $sku + * + * @return array + */ + private function getProductsLinks(string $sku): array + { + $result = []; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($sku, false, null, true); + + foreach ($product->getDownloadableLinks() as $linkObject) { + $result[$linkObject->getLinkId()] = [ + 'title' => $linkObject->getTitle(), + 'price' => $linkObject->getPrice(), + ]; + } + + return $result; + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * + * @return string + */ + private function getQuery( + int $qty, + string $sku, + string $customizableOptions + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: 0, + wishlistItems: [ + { + sku: "{$sku}" + quantity: {$qty} + {$customizableOptions} + } + ] +) { + userInputErrors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items { + id + description + qty + added_at + } + } + } +} +MUTATION; + } + + /** + * Generates Id_v2 for downloadable links + * + * @param int $linkId + * + * @return string + */ + private function generateProductLinkSelectedOptions(int $linkId): string + { + return base64_encode("downloadable/$linkId"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php index 2208f904320d9..0a8e1757a2ce2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php @@ -32,6 +32,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Wishlist/_files/wishlist.php */ public function testCustomerWishlist(): void @@ -74,6 +75,7 @@ public function testCustomerWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Customer/_files/customer.php */ public function testCustomerAlwaysHasWishlist(): void @@ -100,6 +102,7 @@ public function testCustomerAlwaysHasWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 */ public function testGuestCannotGetWishlist() { @@ -121,6 +124,35 @@ public function testGuestCannotGetWishlist() $this->graphQlQuery($query); } + /** + * @magentoConfigFixture default_store wishlist/general/active 0 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCustomerCannotGetWishlistWhenDisabled() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The wishlist is not currently available.'); + + $query = + <<<QUERY +{ + customer { + wishlist { + items_count + sharing_code + updated_at + } + } +} +QUERY; + $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + } + /** * @param string $email * @param string $password diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php new file mode 100644 index 0000000000000..2e203e3ff4228 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test coverage for deleting a product from wishlist + */ +class DeleteProductsFromWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + */ + public function testDeleteWishlistItemFromWishlist(): void + { + $wishlist = $this->getWishlist(); + $wishlistId = $wishlist['customer']['wishlist']['id']; + $wishlist = $wishlist['customer']['wishlist']; + $wishlistItems = $wishlist['items']; + self::assertEquals(1, $wishlist['items_count']); + + $query = $this->getQuery((int) $wishlistId, (int) $wishlistItems[0]['id']); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('removeProductsFromWishlist', $response); + self::assertArrayHasKey('wishlist', $response['removeProductsFromWishlist']); + $wishlistResponse = $response['removeProductsFromWishlist']['wishlist']; + self::assertEquals(0, $wishlistResponse['items_count']); + self::assertEmpty($wishlistResponse['items']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $wishlistId + * @param int $wishlistItemId + * + * @return string + */ + private function getQuery( + int $wishlistId, + int $wishlistItemId + ): string { + return <<<MUTATION +mutation { + removeProductsFromWishlist( + wishlistId: {$wishlistId}, + wishlistItemsIds: [{$wishlistItemId}] +) { + userInputErrors { + code + message + } + wishlist { + id + sharing_code + items_count + items { + id + description + qty + } + } + } +} +MUTATION; + } + + /** + * Get wishlist result + * + * @return array + * + * @throws Exception + */ + public function getWishlist(): array + { + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + } + + /** + * Get customer wishlist query + * + * @return string + */ + private function getCustomerWishlistQuery(): string + { + return <<<QUERY +query { + customer { + wishlist { + id + items_count + items { + id + qty + description + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php new file mode 100644 index 0000000000000..6d54d9f0b4444 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; + +/** + * Generate an array with test values for customizable options with encoded id_v2 value + */ +class GetCustomOptionsWithIDV2ForQueryBySku +{ + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $productCustomOptionRepository; + + /** + * @param ProductCustomOptionRepositoryInterface $productCustomOptionRepository + */ + public function __construct(ProductCustomOptionRepositoryInterface $productCustomOptionRepository) + { + $this->productCustomOptionRepository = $productCustomOptionRepository; + } + + /** + * Returns array of custom options for the product + * + * @param string $sku + * + * @return array + */ + public function execute(string $sku): array + { + $customOptions = $this->productCustomOptionRepository->getList($sku); + $selectedOptions = []; + $enteredOptions = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + + if ($optionType === 'field' || $optionType === 'area' || $optionType === 'date') { + $enteredOptions[] = [ + 'id' => $this->encodeEnteredOption((int)$customOption->getOptionId()), + 'value' => '2012-12-12' + ]; + } elseif ($optionType === 'drop_down') { + $optionSelectValues = $customOption->getValues(); + $selectedOptions[] = $this->encodeSelectedOption( + (int)$customOption->getOptionId(), + (int)reset($optionSelectValues)->getOptionTypeId() + ); + } elseif ($optionType === 'multiple') { + foreach ($customOption->getValues() as $optionValue) { + $selectedOptions[] = $this->encodeSelectedOption( + (int)$customOption->getOptionId(), + (int)$optionValue->getOptionTypeId() + ); + } + } + } + + return [ + 'selected_options' => $selectedOptions, + 'entered_options' => $enteredOptions + ]; + } + + /** + * Returns id_v2 of the selected custom option + * + * @param int $optionId + * @param int $optionValueId + * + * @return string + */ + private function encodeSelectedOption(int $optionId, int $optionValueId): string + { + return base64_encode("custom-option/$optionId/$optionValueId"); + } + + /** + * Returns id_v2 of the entered custom option + * + * @param int $optionId + * + * @return string + */ + private function encodeEnteredOption(int $optionId): string + { + return base64_encode("custom-option/$optionId"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php new file mode 100644 index 0000000000000..9e96bdc5d7079 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test coverage for updating a product from wishlist + */ +class UpdateProductsFromWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + */ + public function testUpdateSimpleProductFromWishlist(): void + { + $wishlist = $this->getWishlist(); + $qty = 5; + $description = 'New Description'; + $wishlistId = $wishlist['customer']['wishlist']['id']; + $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; + self::assertNotEquals($description, $wishlistItem['description']); + self::assertNotEquals($qty, $wishlistItem['qty']); + + $query = $this->getQuery((int) $wishlistId, (int) $wishlistItem['id'], $qty, $description); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('updateProductsInWishlist', $response); + self::assertArrayHasKey('wishlist', $response['updateProductsInWishlist']); + $wishlistResponse = $response['updateProductsInWishlist']['wishlist']; + self::assertEquals($qty, $wishlistResponse['items'][0]['qty']); + self::assertEquals($description, $wishlistResponse['items'][0]['description']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $wishlistId + * @param int $wishlistItemId + * @param int $qty + * @param string $description + * + * @return string + */ + private function getQuery( + int $wishlistId, + int $wishlistItemId, + int $qty, + string $description + ): string { + return <<<MUTATION +mutation { + updateProductsInWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + wishlist_item_id: "{$wishlistItemId}" + quantity: {$qty} + description: "{$description}" + } + ] +) { + userInputErrors { + code + message + } + wishlist { + id + sharing_code + items_count + items { + id + description + qty + } + } + } +} +MUTATION; + } + + /** + * Get wishlist result + * + * @return array + * + * @throws Exception + */ + public function getWishlist(): array + { + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + } + + /** + * Get customer wishlist query + * + * @return string + */ + private function getCustomerWishlistQuery(): string + { + return <<<QUERY +query { + customer { + wishlist { + id + items_count + items { + id + qty + description + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php index bb353938239bc..88c59d6dd8428 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php @@ -39,6 +39,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Wishlist/_files/wishlist.php */ public function testGetCustomerWishlist(): void @@ -94,6 +95,7 @@ public function testGetCustomerWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 */ public function testGetGuestWishlist() { diff --git a/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php b/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php index 91a044f189b4c..0e277ac942263 100644 --- a/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php @@ -7,7 +7,7 @@ namespace Magento\Integration\Model; use Magento\Customer\Api\AccountManagementInterface; -use Magento\Framework\Exception\InputException; +use Magento\Framework\Webapi\Rest\Request; use Magento\Integration\Model\Oauth\Token as TokenModel; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; @@ -76,9 +76,15 @@ protected function setUp(): void } /** + * Create customer access token + * + * @dataProvider storesDataProvider * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @param string|null $store + * @return void */ - public function testCreateCustomerAccessToken() + public function testCreateCustomerAccessToken(?string $store): void { $userName = 'customer@example.com'; $password = 'password'; @@ -86,15 +92,28 @@ public function testCreateCustomerAccessToken() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $userName, 'password' => $password]; - $accessToken = $this->_webApiCall($serviceInfo, $requestData); + $accessToken = $this->_webApiCall($serviceInfo, $requestData, null, $store); $this->assertToken($accessToken, $userName, $password); } + /** + * DataProvider for testCreateCustomerAccessToken + * + * @return array + */ + public function storesDataProvider(): array + { + return [ + 'default store' => [null], + 'all store view' => ['all'], + ]; + } + /** * @dataProvider validationDataProvider */ @@ -105,7 +124,7 @@ public function testCreateCustomerAccessTokenEmptyOrNullCredentials($username, $ $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $username, 'password' => $password]; @@ -128,7 +147,7 @@ public function testCreateCustomerAccessTokenInvalidCustomer() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $customerUserName, 'password' => $password]; @@ -195,7 +214,7 @@ public function testThrottlingMaxAttempts() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $invalidCredentials = [ @@ -238,7 +257,7 @@ public function testThrottlingAccountLockout() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $invalidCredentials = [ diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php index e5df8c18cda0c..e7ee1acda7982 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php @@ -4,27 +4,64 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Service\V1; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Test for hold order. + */ class OrderHoldTest extends WebapiAbstract { - const SERVICE_VERSION = 'V1'; + private const SERVICE_VERSION = 'V1'; - const SERVICE_NAME = 'salesOrderManagementV1'; + private const SERVICE_NAME = 'salesOrderManagementV1'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + } /** - * @magentoApiDataFixture Magento/Sales/_files/order.php + * Test hold order and check order items product options after. + * + * @magentoApiDataFixture Magento/Sales/_files/order_with_two_configurable_variations.php + * + * @return void */ - public function testOrderHold() + public function testOrderHold(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $order = $objectManager->get(\Magento\Sales\Model\Order::class)->loadByIncrementId('100000001'); + $order = $this->objectManager->get(Order::class) + ->loadByIncrementId('100000001'); + $orderId = $order->getId(); + $orderItemsProductOptions = $this->getOrderItemsProductOptions($order); + $serviceInfo = [ 'rest' => [ - 'resourcePath' => '/V1/orders/' . $order->getId() . '/hold', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'resourcePath' => '/V1/orders/' . $orderId . '/hold', + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -32,8 +69,29 @@ public function testOrderHold() 'operation' => self::SERVICE_NAME . 'hold', ], ]; - $requestData = ['id' => $order->getId()]; + $requestData = ['id' => $orderId]; $result = $this->_webApiCall($serviceInfo, $requestData); $this->assertTrue($result); + + $this->assertEquals( + $orderItemsProductOptions, + $this->getOrderItemsProductOptions($this->orderRepository->get($orderId)) + ); + } + + /** + * Return order items product options + * + * @param OrderInterface $order + * @return array + */ + private function getOrderItemsProductOptions(OrderInterface $order): array + { + $result = []; + foreach ($order->getItems() as $orderItem) { + $result[] = $orderItem->getProductOptions(); + } + + return $result; } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php index 13437554febd3..0d2f9d63c5d7f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php @@ -49,7 +49,7 @@ class CategoryTest extends TestCase */ protected $objectManager; - /** @var CategoryRepository */ + /** @var CategoryResource */ private $categoryResource; /** @var CategoryRepositoryInterface */ @@ -355,6 +355,17 @@ public function testDeleteChildren(): void $this->assertEquals($this->_model->getId(), null); } + /** + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/categories_no_products.php + */ + public function testChildrenCountAfterDeleteParentCategory(): void + { + $this->categoryRepository->deleteByIdentifier(3); + $this->assertEquals(8, $this->categoryResource->getChildrenCount(1)); + } + /** * @magentoDataFixture Magento/Catalog/_files/category.php */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php index 4ed783100fa98..7c8ce4c63034d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php @@ -20,6 +20,9 @@ $entityTypeId = $entityModel->setType(\Magento\Catalog\Model\Product::ENTITY)->getTypeId(); $groupId = $installer->getDefaultAttributeGroupId($entityTypeId, $attributeSetId); +/** @var \Magento\Catalog\Model\Product $product */ +$product = $productRepository->get('simple', true); + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ $attribute = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $attribute->setAttributeCode( @@ -30,6 +33,8 @@ 'text' )->setFrontendLabel( 'custom_attributes_frontend_label' +)->setAttributeSetId( + $product->getDefaultAttributeSetId() )->setAttributeGroupId( $groupId )->setIsFilterable( @@ -40,8 +45,6 @@ $attribute->getBackendTypeByInput($attribute->getFrontendInput()) )->save(); -$product = $productRepository->get('simple', true); - $product->setCustomAttribute($attribute->getAttributeCode(), 'customAttributeValue'); $productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 9dee418f010a8..d3f012bb0852f 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -3196,4 +3196,29 @@ public function testImportProductsWithLinksInDifferentBunches() } $this->assertEquals($linksData, $importedProductLinks); } + + /** + * Tests that image name does not have to be prefixed by slash + * + * @magentoDataFixture mediaImportImageFixture + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + */ + public function testUpdateImageByNameNotPrefixedWithSlash() + { + $expectedLabelForDefaultStoreView = 'image label updated'; + $expectedImageFile = '/m/a/magento_image.jpg'; + $secondStoreCode = 'fixturestore'; + $productSku = 'simple'; + $this->importDataForMediaTest('import_image_name_without_slash.csv'); + $product = $this->getProductBySku($productSku); + $imageItems = $product->getMediaGalleryImages()->getItems(); + $this->assertCount(1, $imageItems); + $imageItem = array_shift($imageItems); + $this->assertEquals($expectedImageFile, $imageItem->getFile()); + $this->assertEquals($expectedLabelForDefaultStoreView, $imageItem->getLabel()); + $product = $this->getProductBySku($productSku, $secondStoreCode); + $imageItems = $product->getMediaGalleryImages()->getItems(); + $this->assertCount(0, $imageItems); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv new file mode 100644 index 0000000000000..415501daf89d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv @@ -0,0 +1,3 @@ +"sku","store_view_code","base_image","base_image_label","hide_from_product_page" +"simple",,"m/a/magento_image.jpg","image label updated", +"simple","fixturestore",,,"m/a/magento_image.jpg" diff --git a/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php b/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php index 8aad6049125a8..660d59f3264ec 100644 --- a/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php +++ b/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php @@ -25,7 +25,7 @@ use PHPUnit\Framework\TestCase; /** - * Test for plugin which is listening store resource model and on save replace cms page url rewrites + * Test for plugin which is listening store resource model and on save replace cms page url rewrites. * * @magentoAppArea adminhtml * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -84,9 +84,10 @@ protected function setUp(): void /** * Test of replacing cms page url rewrites on create and delete store * + * @magentoDataFixture Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php * @magentoDataFixture Magento/Cms/_files/pages.php */ - public function testUrlRewritesChangesAfterStoreSave() + public function testUrlRewritesChangesAfterStoreSave(): void { $storeId = $this->createStore(); $this->assertUrlRewritesCount($storeId, 'page100', 1); @@ -98,16 +99,16 @@ public function testUrlRewritesChangesAfterStoreSave() } /** - * Assert url rewrites count by store id + * Assert url rewrites count by store id and request path * * @param int $storeId - * @param string $pageIdentifier + * @param string $requestPath * @param int $expectedCount */ - private function assertUrlRewritesCount(int $storeId, string $pageIdentifier, int $expectedCount): void + private function assertUrlRewritesCount(int $storeId, string $requestPath, int $expectedCount): void { $data = [ - UrlRewrite::REQUEST_PATH => $pageIdentifier, + UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::STORE_ID => $storeId ]; $urlRewrites = $this->urlFinder->findAllByData($data); @@ -116,8 +117,6 @@ private function assertUrlRewritesCount(int $storeId, string $pageIdentifier, in /** * Create test store - * - * @return int */ private function createStore(): int { @@ -134,7 +133,6 @@ private function createStore(): int * Delete test store * * @param int $storeId - * @return void */ private function deleteStore(int $storeId): void { diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php index e6d3c5aa39d15..1d06aa7201f64 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php @@ -138,6 +138,22 @@ public function testFaxEnabled(): void $this->assertStringContainsString('title="Fax"', $block->toHtml()); } + /** + * @magentoDataFixture Magento/Customer/_files/attribute_city_store_label_address.php + */ + public function testCityWithStoreLabel(): void + { + /** @var \Magento\Customer\Block\Form\Register $block */ + $block = Bootstrap::getObjectManager()->create( + Register::class + )->setTemplate('Magento_Customer::form/register.phtml') + ->setShowAddressFields(true); + $this->setAttributeDataProvider($block); + + $this->assertStringNotContainsString('title="City"', $block->toHtml()); + $this->assertStringContainsString('title="Suburb"', $block->toHtml()); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php new file mode 100644 index 0000000000000..8a4afc23aaea8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +//@codingStandardsIgnoreFile +/** @var \Magento\Customer\Model\Attribute $model */ +$model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Attribute::class); +/** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ +$storeManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\StoreManager::class); +$model->loadByCode('customer_address', 'city'); +$storeLabels = $model->getStoreLabels(); +$stores = $storeManager->getStores(); +/** @var \Magento\Store\Api\Data\WebsiteInterface $website */ +foreach ($stores as $store) { + $storeLabels[$store->getId()] = 'Suburb'; +} +$model->setStoreLabels($storeLabels); +$model->save(); diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php new file mode 100644 index 0000000000000..4cbe088893b03 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\GiftMessage\Model\Message; +use Magento\GiftMessage\Model\ResourceModel\Message as MessageResource; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResource; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products.php'); + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var QuoteResource $quote */ +$quote = $objectManager->create(QuoteResource::class); + +/** @var Quote $quoteModel */ +$quoteModel = $objectManager->create(Quote::class); +$quoteModel->setData(['store_id' => 1, 'is_active' => 1, 'is_multi_shipping' => 0]); +$quote->save($quoteModel); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); + +$quoteModel->setReservedOrderId('test_guest_order_with_gift_message') + ->addProduct($product, 1); +$quoteModel->collectTotals(); +$quote->save($quoteModel); + +/** @var MessageResource $message */ +$message = $objectManager->create(MessageResource::class); + +/** @var Message $message */ +$messageModel = $objectManager->create(Message::class); + +$messageModel->setSender('John Doe'); +$messageModel->setRecipient('Jane Roe'); +$messageModel->setMessage('Gift Message Text'); +$message->save($messageModel); + +$quoteModel->getItemByProduct($product)->setGiftMessageId($messageModel->getId()); +$quote->save($quoteModel); + +/** @var QuoteIdMaskResource $quoteIdMask */ +$quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) + ->create(); + +/** @var QuoteIdMask $quoteIdMaskModel */ +$quoteIdMaskModel = $objectManager->create(QuoteIdMask::class); + +$quoteIdMaskModel->setQuoteId($quoteModel->getId()); +$quoteIdMaskModel->setDataChanges(true); +$quoteIdMask->save($quoteIdMaskModel); diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php new file mode 100644 index 0000000000000..9c215cb432b45 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Model\Product; +use Magento\Framework\Registry; +use Magento\GiftMessage\Model\Message; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; + +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$objectManager = Bootstrap::getObjectManager(); +$quote = $objectManager->create(Quote::class); +$quote->load('test_guest_order_with_gift_message', 'reserved_order_id'); +$message = $objectManager->create(Message::class); +$product = $objectManager->create(Product::class); +foreach ($quote->getAllItems() as $item) { + $message->load($item->getGiftMessageId()); + $message->delete(); + $sku = $item->getSku(); + $product->load($product->getIdBySku($sku)); + if ($product->getId()) { + $product->delete(); + } +} +$quote->delete(); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php new file mode 100644 index 0000000000000..2603e2056f19d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\Data\TaxRateInterface; +use Magento\Tax\Api\Data\TaxRuleInterface; +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Api\DataObjectHelper; + +$objectManager = Bootstrap::getObjectManager(); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var Rate $rate */ +$rate = $rateFactory->create(); +$rateData = [ + Rate::KEY_COUNTRY_ID => 'US', + Rate::KEY_REGION_ID => '1', + Rate::KEY_POSTCODE => '*', + Rate::KEY_CODE => 'US-AL-*-Rate-1', + Rate::KEY_PERCENTAGE_RATE => '5.5', +]; +$dataObjectHelper->populateWithArray($rate, $rateData, TaxRateInterface::class); +$rateRepository->save($rate); + +$rule = $ruleFactory->create(); +$ruleData = [ + Rule::KEY_CODE=> 'GraphQl Test Rule AL', + Rule::KEY_PRIORITY => '0', + Rule::KEY_POSITION => '0', + Rule::KEY_CUSTOMER_TAX_CLASS_IDS => [3], + Rule::KEY_PRODUCT_TAX_CLASS_IDS => [2], + Rule::KEY_TAX_RATE_IDS => [$rate->getId()], +]; +$dataObjectHelper->populateWithArray($rule, $ruleData, TaxRuleInterface::class); +$ruleRepository->save($rule); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php new file mode 100644 index 0000000000000..22372f3a21022 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Tax\Model\ResourceModel\Calculation\Rate as RateResource; +use Magento\Tax\Model\ResourceModel\Calculation\Rule as RuleResource; + +$objectManager = Bootstrap::getObjectManager(); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var RateResource $rateResource */ +$rateResource = $objectManager->get(RateResource::class); +/** @var RuleResource $ruleResource */ +$ruleResource = $objectManager->get(RuleResource::class); + +$rate = $rateFactory->create(); +$rateResource->load($rate, 'US-AL-*-Rate-1', Rate::KEY_CODE); +$rule = $ruleFactory->create(); +$ruleResource->load($rule, 'GraphQl Test Rule AL', Rule::KEY_CODE); +$ruleRepository->delete($rule); +$rateRepository->delete($rate); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php index 9d83b3d2ece98..1bd41b047163a 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php @@ -9,6 +9,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\ImportExport\Model\Import; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -28,43 +29,53 @@ class CsvTest extends TestCase */ private $objectManager; - /** - * @var Csv - */ - private $csv; - /** * @inheritdoc */ protected function setUp(): void { - parent::setUp(); - $this->objectManager = Bootstrap::getObjectManager(); - $this->csv = $this->objectManager->create( - Csv::class, - ['destination' => $this->destination] - ); } /** * Test to destruct export adapter + * + * @dataProvider destructDataProvider + * + * @param string $destination + * @param bool $shouldBeDeleted + * @return void */ - public function testDestruct(): void + public function testDestruct(string $destination, bool $shouldBeDeleted): void { + $csv = $this->objectManager->create(Csv::class, ['destination' => $destination]); /** @var Filesystem $fileSystem */ $fileSystem = $this->objectManager->get(Filesystem::class); $directoryHandle = $fileSystem->getDirectoryRead(DirectoryList::VAR_DIR); /** Assert that the destination file is present after construct */ $this->assertFileExists( - $directoryHandle->getAbsolutePath($this->destination), + $directoryHandle->getAbsolutePath($destination), 'The destination file was\'t created after construct' ); - /** Assert that the destination file was removed after destruct */ - $this->csv = null; - $this->assertFileNotExists( - $directoryHandle->getAbsolutePath($this->destination), - 'The destination file was\'t removed after destruct' - ); + unset($csv); + + if ($shouldBeDeleted) { + $this->assertFileDoesNotExist($directoryHandle->getAbsolutePath($destination)); + } else { + $this->assertFileExists($directoryHandle->getAbsolutePath($destination)); + } + } + + /** + * DataProvider for testDestruct + * + * @return array + */ + public function destructDataProvider(): array + { + return [ + 'temporary file' => [$this->destination, true], + 'import history file' => [Import::IMPORT_HISTORY_DIR . $this->destination, false], + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php index dd4fdde250c03..b6508e3b3dfda 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php @@ -53,7 +53,7 @@ public function testGetFilters(): void ['is_filterable' => '1'], [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 1], ], 'Category 1' ); diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php index 07882b68d62d5..e226881b9cfcc 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php @@ -76,7 +76,7 @@ public function getFiltersDataProvider(): array ], [ 'label' => '<span class="price">$60.00</span> and above', - 'value' => '60-', + 'value' => '60-70', 'count' => 1, ], ], @@ -94,7 +94,7 @@ public function getFiltersDataProvider(): array ], [ 'label' => '<span class="price">$50.00</span> and above', - 'value' => '50-', + 'value' => '50-60', 'count' => 1, ], ], diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php index 3b2673b18635a..97928463620f4 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php @@ -71,15 +71,15 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], ['label' => '$20.00 - $29.99', 'value' => '20-30', 'count' => 1], - ['label' => '$50.00 and above', 'value' => '50-', 'count' => 1], + ['label' => '$50.00 and above', 'value' => '50-60', 'count' => 1], ], ], 'auto_calculation_variation_with_big_price_difference' => [ 'config' => ['catalog/layered_navigation/price_range_calculation' => 'auto'], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 300.00], 'expectation' => [ - ['label' => '$0.00 - $99.99', 'value' => '-100', 'count' => 2], - ['label' => '$300.00 and above', 'value' => '300-', 'count' => 1], + ['label' => '$0.00 - $99.99', 'value' => '0-100', 'count' => 2], + ['label' => '$300.00 and above', 'value' => '300-400', 'count' => 1], ], ], 'auto_calculation_variation_with_fixed_price_step' => [ @@ -88,7 +88,7 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$300.00 - $399.99', 'value' => '300-400', 'count' => 1], ['label' => '$400.00 - $499.99', 'value' => '400-500', 'count' => 1], - ['label' => '$500.00 and above', 'value' => '500-', 'count' => 1], + ['label' => '$500.00 and above', 'value' => '500-600', 'count' => 1], ], ], 'improved_calculation_variation_with_small_price_difference' => [ @@ -98,8 +98,8 @@ public function getFiltersDataProvider(): array ], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 50.00], 'expectation' => [ - ['label' => '$0.00 - $49.99', 'value' => '-50', 'count' => 2], - ['label' => '$50.00 and above', 'value' => '50-', 'count' => 1], + ['label' => '$0.00 - $19.99', 'value' => '0-20', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-50', 'count' => 2], ], ], 'improved_calculation_variation_with_big_price_difference' => [ @@ -109,8 +109,8 @@ public function getFiltersDataProvider(): array ], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 300.00], 'expectation' => [ - ['label' => '$0.00 - $299.99', 'value' => '-300', 'count' => 2.0], - ['label' => '$300.00 and above', 'value' => '300-', 'count' => 1.0], + ['label' => '$0.00 - $19.99', 'value' => '0-20', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-300', 'count' => 2], ], ], 'manual_calculation_with_price_step_200' => [ @@ -121,7 +121,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 300.00, 'simple1001' => 300.00, 'simple1002' => 500.00], 'expectation' => [ ['label' => '$200.00 - $399.99', 'value' => '200-400', 'count' => 2], - ['label' => '$400.00 and above', 'value' => '400-', 'count' => 1], + ['label' => '$400.00 and above', 'value' => '400-600', 'count' => 1], ], ], 'manual_calculation_with_price_step_10' => [ @@ -132,7 +132,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 300.00, 'simple1001' => 300.00, 'simple1002' => 500.00], 'expectation' => [ ['label' => '$300.00 - $309.99', 'value' => '300-310', 'count' => 2], - ['label' => '$500.00 and above', 'value' => '500-', 'count' => 1], + ['label' => '$500.00 and above', 'value' => '500-510', 'count' => 1], ], ], 'manual_calculation_with_number_of_intervals_10' => [ @@ -145,7 +145,7 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], ['label' => '$20.00 - $29.99', 'value' => '20-30', 'count' => 1], - ['label' => '$30.00 and above', 'value' => '30-', 'count' => 1], + ['label' => '$30.00 and above', 'value' => '30-40', 'count' => 1], ], ], 'manual_calculation_with_number_of_intervals_2' => [ @@ -157,7 +157,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 30.00], 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 2], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 2], ], ], ]; diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php index 435dd29e16dfa..760f4031b8844 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php @@ -32,7 +32,7 @@ public function testGetFilters(): void ['is_filterable_in_search' => 1], [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 1], ] ); } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php new file mode 100644 index 0000000000000..3aadad7e9ebec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Product\Plugin; + +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Tests for update quote items plugin + * + * @magentoAppArea adminhtml + */ +class UpdateQuoteItemsTest extends TestCase +{ + /** + * @var GetQuoteByReservedOrderId + */ + private $getQuoteByReservedOrderId; + + /** + * @var ProductRepository + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $objectManager = Bootstrap::getObjectManager(); + $this->getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); + $this->productRepository = $objectManager->get(ProductRepository::class); + } + + /** + * Test to mark the quote as need to recollect and doesn't update the field "updated_at" after change product price + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @return void + */ + public function testMarkQuoteRecollectAfterChangeProductPrice(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $this->assertNotNull($quote); + $this->assertFalse((bool)$quote->getTriggerRecollect()); + $this->assertNotEmpty($quote->getItems()); + $quoteItem = current($quote->getItems()); + $product = $quoteItem->getProduct(); + + $product->setPrice((float)$product->getPrice() + 10); + $this->productRepository->save($product); + + /** @var AdapterInterface $connection */ + $connection = $quote->getResource()->getConnection(); + $select = $connection->select() + ->from( + $connection->getTableName('quote'), + ['updated_at', 'trigger_recollect'] + )->where( + "reserved_order_id = 'test_order_with_simple_product_without_address'" + ); + + $quoteRow = $connection->fetchRow($select); + $this->assertNotEmpty($quoteRow); + $this->assertTrue((bool)$quoteRow['trigger_recollect']); + $this->assertEquals($quote->getUpdatedAt(), $quoteRow['updated_at']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php index 30c4ebbdfa98f..6ac4f65f36e5f 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php @@ -27,7 +27,11 @@ 'discount_step' => 0, 'apply_to_shipping' => 1, 'stop_rules_processing' => 1, - 'website_ids' => [$websiteId] + 'website_ids' => [$websiteId], + 'store_labels' => [ + 'store_id' => 0, + 'store_label' => 'Discount Label for 10% off', + ] ] ); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js index 38728bca39192..a8ea949b52ebc 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js @@ -8,6 +8,7 @@ define([ 'jquery', 'moment', 'mageUtils', + 'mage/calendar', 'Magento_Ui/js/lib/knockout/bindings/datepicker' ], function (ko, $, moment, utils) { 'use strict'; @@ -18,6 +19,7 @@ define([ config; beforeEach(function () { + jasmine.clock().install(); element = $('<input />'); observable = ko.observable(); @@ -38,6 +40,7 @@ define([ }); afterEach(function () { + jasmine.clock().uninstall(); element.remove(); }); @@ -62,6 +65,8 @@ define([ expectedDate = moment(date, utils.convertToMomentFormat(inputFormat)).toDate(); observable(date); + jasmine.clock().tick(100); + expect(expectedDate.valueOf()).toEqual(element.datepicker('getDate').valueOf()); }); @@ -69,6 +74,8 @@ define([ element.datepicker('setTimezoneDate').blur().trigger('change'); observable(''); + jasmine.clock().tick(100); + expect(null).toEqual(element.datepicker('getDate')); }); }); diff --git a/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php b/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php index a3e9ed61824ed..c4f6c67200b2b 100644 --- a/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php +++ b/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php @@ -44,7 +44,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getItems( BucketInterface $bucket, @@ -64,13 +64,12 @@ public function getItems( $aggregations['count'] ); - $this->algorithm->setLimits($aggregations['min'], $aggregations['max'] + 0.01); + $this->algorithm->setLimits($aggregations['min'], $aggregations['max']); $interval = $this->dataProvider->getInterval($bucket, $dimensions, $entityStorage); $data = $this->algorithm->calculateSeparators($interval); - $data[0]['from'] = ''; // We should not calculate min and max value - $data[count($data) - 1]['to'] = ''; + $data[0]['from'] = 0; $dataSize = count($data); for ($key = 0; $key < $dataSize; $key++) { diff --git a/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php b/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php index 3bd0677c6a443..3929c4cb47573 100644 --- a/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php +++ b/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php @@ -92,9 +92,31 @@ private function getMca() */ public function isCurrent() { + $urlByPath = preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getPath())); return $this->getCurrent() || - preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getPath())) - == preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getMca())); + ($urlByPath == preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getMca()))) || + $this->isCurrentCmsUrl($urlByPath); + } + + /** + * Get Current displayed page url + * + * @return string + */ + private function getCurrentUrl() + { + return $this->getUrl('*/*/*', ['_current' => false, '_use_rewrite' => true]); + } + + /** + * Check if link URL equivalent to URL of currently displayed CMS page + * + * @param string $urlByPath + * @return bool + */ + private function isCurrentCmsUrl($urlByPath) + { + return ($urlByPath == preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getCurrentUrl())); } /** diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php index b6bf31f580ab6..852cb901e430e 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php @@ -14,109 +14,159 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @covers \Magento\Framework\View\Element\Html\Link\Current + */ class CurrentTest extends TestCase { /** - * @var MockObject + * @var UrlInterface|MockObject */ - protected $_urlBuilderMock; + private $_urlBuilderMock; /** - * @var MockObject + * @var Http|MockObject */ - protected $_requestMock; + private $_requestMock; /** - * @var ObjectManager + * @var Current */ - protected $_objectManager; + private $currentLink; + /** + * @inheritDoc + */ protected function setUp(): void { - $this->_objectManager = new ObjectManager($this); - $this->_urlBuilderMock = $this->getMockForAbstractClass(UrlInterface::class); + $this->_urlBuilderMock = $this->createMock(UrlInterface::class); $this->_requestMock = $this->createMock(Http::class); + + $this->currentLink = (new ObjectManager($this))->getObject( + Current::class, + [ + 'urlBuilder' => $this->_urlBuilderMock, + 'request' => $this->_requestMock + ] + ); } - public function testGetUrl() + /** + * Test get Url + */ + public function testGetUrl(): void { - $path = 'test/path'; - $url = 'http://example.com/asdasd'; + $pathStub = 'test/path'; + $urlStub = 'http://example.com/asdasd'; - $this->_urlBuilderMock->expects($this->once())->method('getUrl')->with($path)->willReturn($url); + $this->_urlBuilderMock->expects($this->once()) + ->method('getUrl') + ->with($pathStub) + ->will($this->returnValue($urlStub)); - /** @var Current $link */ - $link = $this->_objectManager->getObject( - Current::class, - ['urlBuilder' => $this->_urlBuilderMock] - ); + $this->currentLink->setPath($pathStub); - $link->setPath($path); - $this->assertEquals($url, $link->getHref()); + $this->assertEquals($urlStub, $this->currentLink->getHref()); } - public function testIsCurrentIfIsset() + /** + * Test if set current + */ + public function testIsCurrentIfIsset(): void { - /** @var Current $link */ - $link = $this->_objectManager->getObject(Current::class); - $link->setCurrent(true); - $this->assertTrue($link->isCurrent()); + $this->currentLink->setCurrent(true); + $this->assertTrue($this->currentLink->isCurrent()); } /** * Test if the current url is the same as link path * - * @return void + * @param string $pathStub + * @param string $urlStub + * @param array $request + * @param bool $expected + * @dataProvider isCurrentDataProvider */ - public function testIsCurrent() + public function testIsCurrent($pathStub, $urlStub, $request, $expected): void { - $path = 'test/index'; - $url = 'http://example.com/test/index'; - - $this->_requestMock->expects($this->once()) + $this->_requestMock->expects($this->any()) ->method('getPathInfo') - ->willReturn('/test/index/'); - $this->_requestMock->expects($this->once()) + ->will($this->returnValue($request['pathInfoStub'])); + $this->_requestMock->expects($this->any()) ->method('getModuleName') - ->willReturn('test'); - $this->_requestMock->expects($this->once()) + ->will($this->returnValue($request['moduleStub'])); + $this->_requestMock->expects($this->any()) ->method('getControllerName') - ->willReturn('index'); - $this->_requestMock->expects($this->once()) + ->will($this->returnValue($request['controllerStub'])); + $this->_requestMock->expects($this->any()) ->method('getActionName') - ->willReturn('index'); + ->will($this->returnValue($request['actionStub'])); + $this->_urlBuilderMock->expects($this->at(0)) ->method('getUrl') - ->with($path) - ->willReturn($url); + ->with($pathStub) + ->will($this->returnValue($urlStub)); $this->_urlBuilderMock->expects($this->at(1)) ->method('getUrl') - ->with('test/index') - ->willReturn($url); - - /** @var Current $link */ - $link = $this->_objectManager->getObject( - Current::class, - [ - 'urlBuilder' => $this->_urlBuilderMock, - 'request' => $this->_requestMock - ] - ); - - $link->setPath($path); - $this->assertTrue($link->isCurrent()); + ->with($request['mcaStub']) + ->will($this->returnValue($request['getUrl'])); + + if ($request['mcaStub'] == '') { + $this->_urlBuilderMock->expects($this->at(2)) + ->method('getUrl') + ->with('*/*/*', ['_current' => false, '_use_rewrite' => true]) + ->will($this->returnValue($urlStub)); + } + + $this->currentLink->setPath($pathStub); + $this->assertEquals($expected, $this->currentLink->isCurrent()); } - public function testIsCurrentFalse() + /** + * Data provider for is current + */ + public function isCurrentDataProvider(): array { - $this->_urlBuilderMock->expects($this->at(0))->method('getUrl')->willReturn('1'); - $this->_urlBuilderMock->expects($this->at(1))->method('getUrl')->willReturn('2'); - - /** @var Current $link */ - $link = $this->_objectManager->getObject( - Current::class, - ['urlBuilder' => $this->_urlBuilderMock, 'request' => $this->_requestMock] - ); - $this->assertFalse($link->isCurrent()); + return [ + 'url with MCA' => [ + 'pathStub' => 'test/path', + 'urlStub' => 'http://example.com/asdasd', + 'requestStub' => [ + 'pathInfoStub' => '/test/index/', + 'moduleStub' => 'test', + 'controllerStub' => 'index', + 'actionStub' => 'index', + 'mcaStub' => 'test/index', + 'getUrl' => 'http://example.com/asdasd/' + ], + 'excepted' => true + ], + 'url with CMS' => [ + 'pathStub' => 'test', + 'urlStub' => 'http://example.com/test', + 'requestStub' => [ + 'pathInfoStub' => '//test//', + 'moduleStub' => 'cms', + 'controllerStub' => 'page', + 'actionStub' => 'view', + 'mcaStub' => '', + 'getUrl' => 'http://example.com/' + ], + 'excepted' => true + ], + 'Test if is current false' => [ + 'pathStub' => 'test/path', + 'urlStub' => 'http://example.com/tests', + 'requestStub' => [ + 'pathInfoStub' => '/test/index/', + 'moduleStub' => 'test', + 'controllerStub' => 'index', + 'actionStub' => 'index', + 'mcaStub' => 'test/index', + 'getUrl' => 'http://example.com/asdasd/' + ], + 'excepted' => false + ] + ]; } } diff --git a/lib/web/mage/adminhtml/browser.js b/lib/web/mage/adminhtml/browser.js index 604680e0bf8d5..74984024b74a0 100644 --- a/lib/web/mage/adminhtml/browser.js +++ b/lib/web/mage/adminhtml/browser.js @@ -376,7 +376,7 @@ define([ * @param {*} folderName */ confirm: function (folderName) { - return $.ajax({ + $.ajax({ url: self.options.newFolderUrl, dataType: 'json', data: { @@ -399,6 +399,8 @@ define([ ); } }, this)); + + return true; } } }); diff --git a/lib/web/mage/ie-class-fixer.js b/lib/web/mage/ie-class-fixer.js index 683090b1d1386..fe07f273a0b58 100644 --- a/lib/web/mage/ie-class-fixer.js +++ b/lib/web/mage/ie-class-fixer.js @@ -3,18 +3,10 @@ * See COPYING.txt for license details. */ -/* eslint-disable strict */ -(function () { - var userAgent = navigator.userAgent, // user agent identifier - html = document.documentElement, // html tag - gap = ''; // gap between classes +define([], function () { + 'use strict'; - if (html.className) { // check if neighbour class exist in html tag - gap = ' '; - } // end if - - if (userAgent.match(/Trident.*rv[ :]*11\./)) { // Special case for IE11 - html.className += gap + 'ie11'; - } // end if - -})(); + if (navigator.userAgent.match(/Trident.*rv[ :]*11\./)) { + document.documentElement.classList.add('ie11'); + } +}); diff --git a/lib/web/mage/touch-slider.js b/lib/web/mage/touch-slider.js new file mode 100644 index 0000000000000..6c468a832895b --- /dev/null +++ b/lib/web/mage/touch-slider.js @@ -0,0 +1,151 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'jquery-ui-modules/slider' +], function ($, _) { + 'use strict'; + + /** + * Adds support for touch events for regular jQuery UI slider. + */ + $.widget('mage.touchSlider', $.ui.slider, { + + /** + * Creates instance of widget. + * + * @override + */ + _create: function () { + _.bindAll( + this, + '_mouseDown', + '_mouseMove', + '_onTouchEnd' + ); + + return this._superApply(arguments); + }, + + /** + * Initializes mouse events on element. + * @override + */ + _mouseInit: function () { + var result = this._superApply(arguments); + + this.element + .off('mousedown.' + this.widgetName) + .on('touchstart.' + this.widgetName, this._mouseDown); + + return result; + }, + + /** + * Elements' 'mousedown' event handler polyfill. + * @override + */ + _mouseDown: function (event) { + var prevDelegate = this._mouseMoveDelegate, + result; + + event = this._touchToMouse(event); + result = this._super(event); + + if (prevDelegate === this._mouseMoveDelegate) { + return result; + } + + $(document) + .off('mousemove.' + this.widgetName) + .off('mouseup.' + this.widgetName); + + $(document) + .on('touchmove.' + this.widgetName, this._mouseMove) + .on('touchend.' + this.widgetName, this._onTouchEnd) + .on('tochleave.' + this.widgetName, this._onTouchEnd); + + return result; + }, + + /** + * Documents' 'mousemove' event handler polyfill. + * + * @override + * @param {Event} event - Touch event object. + */ + _mouseMove: function (event) { + event = this._touchToMouse(event); + + return this._super(event); + }, + + /** + * Documents' 'touchend' event handler. + */ + _onTouchEnd: function (event) { + $(document).trigger('mouseup'); + + return this._mouseUp(event); + }, + + /** + * Removes previously assigned touch handlers. + * + * @override + */ + _mouseUp: function () { + this._removeTouchHandlers(); + + return this._superApply(arguments); + }, + + /** + * Removes previously assigned touch handlers. + * + * @override + */ + _mouseDestroy: function () { + this._removeTouchHandlers(); + + return this._superApply(arguments); + }, + + /** + * Removes touch events from document object. + */ + _removeTouchHandlers: function () { + $(document) + .off('touchmove.' + this.widgetName) + .off('touchend.' + this.widgetName) + .off('touchleave.' + this.widgetName); + }, + + /** + * Adds properties to the touch event to mimic mouse event. + * + * @param {Event} event - Touch event object. + * @returns {Event} + */ + _touchToMouse: function (event) { + var orig = event.originalEvent, + touch = orig.touches[0]; + + return _.extend(event, { + which: 1, + pageX: touch.pageX, + pageY: touch.pageY, + clientX: touch.clientX, + clientY: touch.clientY, + screenX: touch.screenX, + screenY: touch.screenY + }); + } + }); + + return $.mage.touchSlider; +}); diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index 061416fcfd9ea..fe1cac3f076fd 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -46,6 +46,7 @@ use Magento\Setup\Module\SetupFactory; use Magento\Setup\Validator\DbValidator; use Magento\Store\Model\Store; +use Magento\Framework\App\Cache\Manager; /** * Class Installer contains the logic to install Magento application. @@ -1311,8 +1312,8 @@ public function uninstall() */ private function updateCaches($isEnabled, $types = []) { - /** @var \Magento\Framework\App\Cache\Manager $cacheManager */ - $cacheManager = $this->objectManagerProvider->get()->create(\Magento\Framework\App\Cache\Manager::class); + /** @var Manager $cacheManager */ + $cacheManager = $this->objectManagerProvider->get()->create(Manager::class); $availableTypes = $cacheManager->getAvailableTypes(); $types = empty($types) ? $availableTypes : array_intersect($availableTypes, $types); @@ -1331,8 +1332,9 @@ function (string $key) use ($types) { ); $this->log->log('Current status:'); - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $this->log->log(print_r($cacheStatus, true)); + foreach ($cacheStatus as $cache => $status) { + $this->log->log(sprintf('%s: %d', $cache, $status)); + } } /** @@ -1344,8 +1346,8 @@ function (string $key) use ($types) { */ private function cleanCaches() { - /** @var \Magento\Framework\App\Cache\Manager $cacheManager */ - $cacheManager = $this->objectManagerProvider->get()->get(\Magento\Framework\App\Cache\Manager::class); + /** @var Manager $cacheManager */ + $cacheManager = $this->objectManagerProvider->get()->get(Manager::class); $types = $cacheManager->getAvailableTypes(); $cacheManager->clean($types); $this->log->log('Cache cleared successfully'); diff --git a/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php b/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php index 0d12a73434355..8446486c2f104 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php @@ -450,12 +450,12 @@ public function installDataProvider() ['Installing user configuration...'], ['Enabling caches:'], ['Current status:'], - [print_r(['foo' => 1, 'bar' => 1], true)], + ['foo: 1'], + ['bar: 1'], ['Installing data...'], ['Data install/update:'], ['Disabling caches:'], ['Current status:'], - [print_r([], true)], ['Module \'Foo_One\':'], ['Module \'Bar_Two\':'], ['Data post-updates:'], @@ -463,7 +463,6 @@ public function installDataProvider() ['Module \'Bar_Two\':'], ['Enabling caches:'], ['Current status:'], - [print_r([], true)], ['Caches clearing:'], ['Cache cleared successfully'], ['Disabling Maintenance Mode:'], @@ -502,12 +501,12 @@ public function installDataProvider() ['Installing user configuration...'], ['Enabling caches:'], ['Current status:'], - [print_r(['foo' => 1, 'bar' => 1], true)], + ['foo: 1'], + ['bar: 1'], ['Installing data...'], ['Data install/update:'], ['Disabling caches:'], ['Current status:'], - [print_r([], true)], ['Module \'Foo_One\':'], ['Module \'Bar_Two\':'], ['Data post-updates:'], @@ -515,7 +514,6 @@ public function installDataProvider() ['Module \'Bar_Two\':'], ['Enabling caches:'], ['Current status:'], - [print_r([], true)], ['Installing admin user...'], ['Caches clearing:'], ['Cache cleared successfully'],