diff --git a/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php b/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php index e896901211cb1..cec553afe9463 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php +++ b/app/code/Magento/AwsS3/Test/Mftf/Helper/S3FileAssertions.php @@ -149,6 +149,21 @@ public function assertFileExists($filePath, $message = ''): void $this->assertTrue($this->driver->isExists($filePath), $message); } + /** + * Asserts that a file with the given glob pattern exists in the given path on the remote storage system + * + * @param string $path + * @param string $pattern + * @param string $message + * + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function assertGlobbedFileExists($path, $pattern, $message = ""): void + { + $files = $this->driver->search($pattern, $path); + $this->assertNotEmpty($files, $message); + } + /** * Assert a file does not exist on the remote storage system * @@ -206,6 +221,24 @@ public function assertFileContainsString($filePath, $text, $message = ""): void $this->assertStringContainsString($text, $this->driver->fileGetContents($filePath), $message); } + /** + * Asserts that a file with the given glob pattern at the given path on the remote storage system contains a given string + * + * @param string $path + * @param string $pattern + * @param string $text + * @param int $fileIndex + * @param string $message + * @return void + * + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function assertGlobbedFileContainsString($path, $pattern, $text, $fileIndex = 0, $message = ""): void + { + $files = $this->driver->search($pattern, $path); + $this->assertStringContainsString($text, $this->driver->fileGetContents($files[$fileIndex] ?? ''), $message); + } + /** * Assert a file on the remote storage system does not contain a given string * diff --git a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php index c5c08a0552f42..93732b1b52f0c 100644 --- a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php +++ b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php @@ -153,6 +153,7 @@ public function getCurrentProductData() $this->productRenderCollectorComposite ->collect($product, $productRender); $data = $this->hydrator->extract($productRender); + $data['is_available'] = $product->isAvailable(); $currentProductData = [ 'items' => [ diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index b5fa46635f433..1a57ea3adac19 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -39,6 +39,10 @@ TestFooBar foobar + + Simple Product Double Space + simple-product double-space + Pursuit Lumaflex&trade; Tone Band x&trade; diff --git a/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php b/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php index ed0e244c280dc..dbf777c1fe5c5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php +++ b/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php @@ -134,6 +134,22 @@ public function assertFileExists($filePath, $message = ''): void $this->assertTrue($this->driver->isExists($realPath), $message); } + /** + * Asserts that a file with the given glob pattern exists in the given path + * + * @param string $path + * @param string $pattern + * @param string $message + * + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function assertGlobbedFileExists($path, $pattern, $message = ""): void + { + $realPath = $this->expandPath($path); + $files = $this->driver->search($pattern, $realPath); + $this->assertNotEmpty($files, $message); + } + /** * Assert a file does not exist * @@ -195,6 +211,25 @@ public function assertFileContainsString($filePath, $text, $message = ""): void $this->assertStringContainsString($text, $this->driver->fileGetContents($realPath), $message); } + /** + * Asserts that a file with the given glob pattern at the given path contains a given string + * + * @param string $path + * @param string $pattern + * @param string $text + * @param int $fileIndex + * @param string $message + * @return void + * + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function assertGlobbedFileContainsString($path, $pattern, $text, $fileIndex = 0, $message = ""): void + { + $realPath = $this->expandPath($path); + $files = $this->driver->search($pattern, $realPath); + $this->assertStringContainsString($text, $this->driver->fileGetContents($files[$fileIndex] ?? ''), $message); + } + /** * Assert a file does not contain a given string * diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml new file mode 100644 index 0000000000000..c3e939b4155c8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml @@ -0,0 +1,52 @@ + + + + + + + + + + <description value="Admin should be able to see double spaces in the Name and Sku fields in the product grid"/> + <testCaseId value="MC-40725"/> + <useCaseId value="MC-40122"/> + <severity value="AVERAGE"/> + <group value="Catalog"/> + </annotations> + + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProductWithDoubleSpaces" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="cron:run --group=index" stepKey="cronRun"/> + <magentoCLI command="cron:run --group=index" stepKey="cronRunSecondTime"/> + </before> + + <after> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="deleteProduct"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="searchForProduct"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="assertProductName"> + <argument name="column" value="Name"/> + <argument name="value" value="$createProduct.name$"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="assertProductSku"> + <argument name="column" value="SKU"/> + <argument name="value" value="$createProduct.sku$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php index 6026d1462e461..87f5be4b21333 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php @@ -166,6 +166,7 @@ public function testGetCurrentProductDataWithNonEmptyProduct() { $productMock = $this->getMockBuilder(ProductInterface::class) ->disableOriginalConstructor() + ->addMethods(['isAvailable']) ->getMockForAbstractClass(); $productRendererMock = $this->getMockBuilder(ProductRenderInterface::class) ->disableOriginalConstructor() @@ -173,7 +174,6 @@ public function testGetCurrentProductDataWithNonEmptyProduct() $storeMock = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->getMock(); - $this->registryMock->expects($this->once()) ->method('registry') ->with('product') diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml index 88bb578712056..2cd2a15b04900 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml @@ -132,7 +132,7 @@ <settings> <addField>true</addField> <filter>text</filter> - <bodyTmpl>ui/grid/cells/html</bodyTmpl> + <bodyTmpl>Magento_Catalog/grid/cells/preserved</bodyTmpl> <label translate="true">Name</label> </settings> </column> @@ -155,7 +155,7 @@ <column name="sku" sortOrder="60"> <settings> <filter>text</filter> - <bodyTmpl>ui/grid/cells/html</bodyTmpl> + <bodyTmpl>Magento_Catalog/grid/cells/preserved</bodyTmpl> <label translate="true">SKU</label> </settings> </column> diff --git a/app/code/Magento/Catalog/view/adminhtml/web/template/grid/cells/preserved.html b/app/code/Magento/Catalog/view/adminhtml/web/template/grid/cells/preserved.html new file mode 100644 index 0000000000000..936342df23795 --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/template/grid/cells/preserved.html @@ -0,0 +1,7 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="data-grid-cell-content white-space-preserved" html="$col.getLabel($row())"/> diff --git a/app/code/Magento/Catalog/view/base/web/js/product/addtocart-button.js b/app/code/Magento/Catalog/view/base/web/js/product/addtocart-button.js index 4baf082b37c02..f599d05ba5ea9 100644 --- a/app/code/Magento/Catalog/view/base/web/js/product/addtocart-button.js +++ b/app/code/Magento/Catalog/view/base/web/js/product/addtocart-button.js @@ -55,6 +55,16 @@ define([ return row['is_salable']; }, + /** + * Depends on this option, stock status text can be "In stock" or "Out Of Stock" + * + * @param {Object} row + * @returns {Boolean} + */ + isAvailable: function (row) { + return row['is_available']; + }, + /** * Depends on this option, "Add to cart" button can be shown or hide. Depends on backend configuration * diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml index 07cca77178a38..b7d6e1f2079a0 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml @@ -78,7 +78,7 @@ </button> </form> <?php else :?> - <?php if ($item->getIsSalable()) :?> + <?php if ($item->isAvailable()) :?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else :?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml index a831bd7be6f71..4fba22f41c9de 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml @@ -153,7 +153,7 @@ $_helper = $block->getData('outputHelper'); <?php endforeach; ?> </ol> </div> - <?= $block->getToolbarHtml() ?> + <?= $block->getChildBlock('toolbar')->setIsBottom(true)->toHtml() ?> <script type="text/x-magento-init"> { "[data-role=tocart-form], .form.map.checkout": { diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml index e426b940deab7..6fd619de7fd6c 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml @@ -287,7 +287,7 @@ $_item = null; </form> <?php endif; ?> <?php else:?> - <?php if ($_item->getIsSalable()):?> + <?php if ($_item->isAvailable()):?> <div class="stock available"> <span><?= $block->escapeHtml(__('In stock')) ?></span> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml index 76ef6baf4993e..3c8687d090baf 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml @@ -10,27 +10,23 @@ * * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar */ - -// phpcs:disable Magento2.Security.IncludeFile.FoundIncludeFile -// phpcs:disable PSR2.Methods.FunctionCallSignature.SpaceBeforeOpenBracket ?> <?php if ($block->getCollection()->getSize()) :?> <?php $widget = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode($block->getWidgetOptionsJson()); $widgetOptions = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($widget['productListToolbarForm']); ?> <div class="toolbar toolbar-products" data-mage-init='{"productListToolbarForm":<?= /* @noEscape */ $widgetOptions ?>}'> - <?php if ($block->isExpanded()) :?> - <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/viewmode.phtml')) ?> - <?php endif; ?> - - <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/amount.phtml')) ?> - - <?= $block->getPagerHtml() ?> - - <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/limiter.phtml')) ?> - - <?php if ($block->isExpanded()) :?> - <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/sorter.phtml')) ?> - <?php endif; ?> + <?php if ($block->getIsBottom()): ?> + <?= $block->getPagerHtml() ?> + <?= $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/limiter.phtml')) ?> + <?php else: ?> + <?php if ($block->isExpanded()): ?> + <?= $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/viewmode.phtml')) ?> + <?php endif ?> + <?= $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/amount.phtml')) ?> + <?php if ($block->isExpanded()): ?> + <?= $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/sorter.phtml')) ?> + <?php endif ?> + <?php endif ?> </div> <?php endif ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml index 6cebd51284f48..49dd702a6e39c 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml @@ -63,7 +63,7 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); . ' data-mage-init=\'{ "redirectUrl": { "event": "click", url: "' . $block->escapeUrl($block->getAddToCartUrl($_product)) . '"} }\'>' . '<span>' . $block->escapeHtml(__('Add to Cart')) . '</span></button>'; } else { - $info['button'] = $_product->getIsSalable() ? '<div class="stock available"><span>' . $block->escapeHtml(__('In stock')) . '</span></div>' : + $info['button'] = $_product->isAvailable() ? '<div class="stock available"><span>' . $block->escapeHtml(__('In stock')) . '</span></div>' : '<div class="stock unavailable"><span>' . $block->escapeHtml(__('Out of stock')) . '</span></div>'; } diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml index 53a0682311b1f..fce91564c96a2 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml @@ -52,7 +52,7 @@ </button> <?php endif; ?> <?php else :?> - <?php if ($_product->getIsSalable()) :?> + <?php if ($_product->isAvailable()) :?> <div class="stock available" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> <span><?= $block->escapeHtml(__('In stock')) ?></span> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml index 5108c488aec19..66683ef328e08 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml @@ -89,7 +89,7 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> </button> <?php endif; ?> <?php else :?> - <?php if ($_item->getIsSalable()) :?> + <?php if ($_item->isAvailable()) :?> <div class="stock available"> <span><?= $block->escapeHtml(__('In stock')) ?></span> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml index 378cd49493a6e..ceb32e78c7e44 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml @@ -88,7 +88,7 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> </button> <?php endif; ?> <?php else :?> - <?php if ($_item->getIsSalable()) :?> + <?php if ($_item->isAvailable()) :?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else :?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html b/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html index 05dbf02703285..867b5f40d98db 100644 --- a/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html +++ b/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html @@ -15,10 +15,10 @@ </button> </if> - <ifnot args="isSalable($row())"> + <if args="isAvailable($row()) === false"> <div class="stock unavailable"> <text args="$t('Availability')"/> <span translate="'Out of stock'"/> </div> - </ifnot> + </if> </if> diff --git a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml index 9637815c90eef..000f3ffd36934 100644 --- a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml +++ b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml @@ -77,7 +77,7 @@ use Magento\Framework\App\Action\Action; </button> </form> <?php else: ?> - <?php if ($_item->getIsSalable()): ?> + <?php if ($_item->isAvailable()): ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else: ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php new file mode 100644 index 0000000000000..68968b6f3819a --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\ConfigurableProductGraphQl\Model\Formatter; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; +use Magento\Framework\GraphQl\Query\Uid; + +/** + * Formatter for configurable product options + */ +class Option +{ + /** + * @var Uid + */ + private $idEncoder; + + /** + * @var OptionValue + */ + private $valueFormatter; + + /** + * @param Uid $idEncoder + * @param OptionValue $valueFormatter + */ + public function __construct( + Uid $idEncoder, + OptionValue $valueFormatter + ) { + $this->idEncoder = $idEncoder; + $this->valueFormatter = $valueFormatter; + } + + /** + * Format configurable product options according to the GraphQL schema + * + * @param Attribute $attribute + * @param array $optionIds + * @return array|null + */ + public function format(Attribute $attribute, array $optionIds): ?array + { + $optionValues = []; + + foreach ($attribute->getOptions() as $option) { + $optionValues[] = $this->valueFormatter->format($option, $attribute, $optionIds); + } + + return [ + 'uid' => $this->idEncoder->encode($attribute->getProductSuperAttributeId()), + 'attribute_code' => $attribute->getProductAttribute()->getAttributeCode(), + 'label' => $attribute->getLabel(), + 'values' => $optionValues, + ]; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php new file mode 100644 index 0000000000000..5d721f13fbb9d --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\ConfigurableProductGraphQl\Model\Formatter; + +use Magento\CatalogInventory\Model\StockRegistry; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; +use Magento\ConfigurableProductGraphQl\Model\Options\SelectionUidFormatter; + +/** + * Formatter for configurable product option values + */ +class OptionValue +{ + /** + * @var SelectionUidFormatter + */ + private $selectionUidFormatter; + + /** + * @var StockRegistry + */ + private $stockRegistry; + + /** + * @param SelectionUidFormatter $selectionUidFormatter + * @param StockRegistry $stockRegistry + */ + public function __construct( + SelectionUidFormatter $selectionUidFormatter, + StockRegistry $stockRegistry + ) { + $this->selectionUidFormatter = $selectionUidFormatter; + $this->stockRegistry = $stockRegistry; + } + + /** + * Format configurable product option values according to the GraphQL schema + * + * @param array $optionValue + * @param Attribute $attribute + * @param array $optionIds + * @return array + */ + public function format(array $optionValue, Attribute $attribute, array $optionIds): array + { + $valueIndex = (int)$optionValue['value_index']; + $attributeId = (int)$attribute->getAttributeId(); + + return [ + 'uid' => $this->selectionUidFormatter->encode( + $attributeId, + $valueIndex + ), + 'is_available' => $this->getIsAvailable($optionIds[$valueIndex] ?? []), + 'is_use_default' => (bool)$attribute->getIsUseDefault(), + 'label' => $optionValue['label'], + 'value_index' => $optionValue['value_index'] + ]; + } + + /** + * Get is variants available + * + * @param array $variantIds + * @return bool + */ + private function getIsAvailable(array $variantIds): bool + { + foreach ($variantIds as $variantId) { + if ($this->stockRegistry->getProductStockStatus($variantId)) { + return true; + } + } + + return false; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Variant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Variant.php new file mode 100644 index 0000000000000..1d73ad6a19336 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Variant.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\ConfigurableProductGraphQl\Model\Formatter; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Formatter for configurable product variant + */ +class Variant +{ + /** + * Format selected variant of configurable product based on selected options + * + * @param array $options + * @param array $selectedOptions + * @param array $variants + * @return array|null + * @throws GraphQlInputException + */ + public function format(array $options, array $selectedOptions, array $variants): ?array + { + $variant = null; + $productIds = array_keys($variants); + + foreach ($selectedOptions as $attributeId => $selectedValue) { + if (!isset($options[$attributeId][$selectedValue])) { + throw new GraphQlInputException(__('configurableOptionValueUids values are incorrect')); + } + + $productIds = array_intersect($productIds, $options[$attributeId][$selectedValue]); + } + + if (count($productIds) === 1) { + $variantProduct = $variants[array_pop($productIds)]; + $variant = $variantProduct->getData(); + $variant['url_path'] = $variantProduct->getProductUrl(); + $variant['model'] = $variantProduct; + } + + return $variant; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php new file mode 100644 index 0000000000000..ce81e970bcd58 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\ConfigurableProductGraphQl\Model\Options; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\ConfigurableProduct\Helper\Data; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; +use Magento\ConfigurableProductGraphQl\Model\Formatter\Option; + +/** + * Retrieve metadata for configurable option selection. + */ +class ConfigurableOptionsMetadata +{ + /** + * @var Data + */ + private $configurableProductHelper; + + /** + * @var Option + */ + private $configurableOptionsFormatter; + + /** + * @param Data $configurableProductHelper + * @param Option $configurableOptionsFormatter + */ + public function __construct( + Data $configurableProductHelper, + Option $configurableOptionsFormatter + ) { + $this->configurableProductHelper = $configurableProductHelper; + $this->configurableOptionsFormatter = $configurableOptionsFormatter; + } + + /** + * Load available selections from configurable options and variant. + * + * @param ProductInterface $product + * @param array $options + * @param array $selectedOptions + * @return array + */ + public function getAvailableSelections(ProductInterface $product, array $options, array $selectedOptions): array + { + $attributes = $this->getAttributes($product); + $availableSelections = []; + + foreach ($options as $attributeId => $option) { + if ($attributeId === 'index' || isset($selectedOptions[$attributeId])) { + continue; + } + + $availableSelections[] = $this->configurableOptionsFormatter->format( + $attributes[$attributeId], + $options[$attributeId] ?? [] + ); + } + + return $availableSelections; + } + + /** + * Retrieve configurable attributes for the product + * + * @param ProductInterface $product + * @return Attribute[] + */ + private function getAttributes(ProductInterface $product): array + { + $allowedAttributes = $this->configurableProductHelper->getAllowAttributes($product); + $attributes = []; + foreach ($allowedAttributes as $attribute) { + $attributes[$attribute->getAttributeId()] = $attribute; + } + + return $attributes; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php index 80fbdc76bacb3..7b5a3fb806a5f 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogInventory\Model\ResourceModel\Stock\StatusFactory; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Framework\Exception\LocalizedException; /** * Retrieve child products @@ -45,7 +46,7 @@ public function __construct( * @return ProductInterface[] * @throws \Magento\Framework\Exception\LocalizedException */ - public function getSalableVariantsByParent(ProductInterface $product) + public function getSalableVariantsByParent(ProductInterface $product): array { $collection = $this->configurableType->getUsedProductCollection($product); $collection @@ -62,6 +63,6 @@ public function getSalableVariantsByParent(ProductInterface $product) } $collection->clear(); - return $collection->getItems(); + return $collection->getItems() ?? []; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php index 1d13ad75489a1..8c82c9414763f 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ConfigurableProductGraphQl\Model\Options; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Uid; + /** * Handle option selection uid. */ @@ -20,6 +24,19 @@ class SelectionUidFormatter */ private const UID_SEPARATOR = '/'; + /** + * @var Uid + */ + private $idEncoder; + + /** + * @param Uid $idEncoder + */ + public function __construct(Uid $idEncoder) + { + $this->idEncoder = $idEncoder; + } + /** * Create uid and encode. * @@ -29,28 +46,22 @@ class SelectionUidFormatter */ public function encode(int $attributeId, int $indexId): string { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - return base64_encode(implode(self::UID_SEPARATOR, [ - self::UID_PREFIX, - $attributeId, - $indexId - ])); + return $this->idEncoder->encode(implode(self::UID_SEPARATOR, [self::UID_PREFIX, $attributeId, $indexId])); } /** * Retrieve attribute and option index from uid. Array key is the id of attribute and value is the index of option * - * @param string $selectionUids + * @param array $selectionUids * @return array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @throws GraphQlInputException */ public function extract(array $selectionUids): array { $attributeOption = []; foreach ($selectionUids as $uid) { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $optionData = explode(self::UID_SEPARATOR, base64_decode($uid)); - if (count($optionData) == 3) { + $optionData = explode(self::UID_SEPARATOR, $this->idEncoder->decode($uid)); + if (count($optionData) === 3) { $attributeOption[(int)$optionData[1]] = (int)$optionData[2]; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php index 6aa3a9774b439..556aab7e39f7d 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php @@ -3,17 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableProductGraphQl\Model\Resolver; -use Magento\Catalog\Api\Data\ProductInterface; +use Magento\ConfigurableProduct\Helper\Data; +use Magento\ConfigurableProductGraphQl\Model\Formatter\Variant as VariantFormatter; +use Magento\ConfigurableProductGraphQl\Model\Options\ConfigurableOptionsMetadata; +use Magento\ConfigurableProductGraphQl\Model\Options\DataProvider\Variant; use Magento\ConfigurableProductGraphQl\Model\Options\Metadata; +use Magento\ConfigurableProductGraphQl\Model\Options\SelectionUidFormatter; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** - * Resolver class for option selection metadata. + * Resolver for options selection */ class OptionsSelectionMetadata implements ResolverInterface { @@ -22,13 +28,53 @@ class OptionsSelectionMetadata implements ResolverInterface */ private $configurableSelectionMetadata; + /** + * @var ConfigurableOptionsMetadata + */ + private $configurableOptionsMetadata; + + /** + * @var SelectionUidFormatter + */ + private $selectionUidFormatter; + + /** + * @var Variant + */ + private $variant; + + /** + * @var VariantFormatter + */ + private $variantFormatter; + + /** + * @var Data + */ + private $configurableProductHelper; + /** * @param Metadata $configurableSelectionMetadata + * @param ConfigurableOptionsMetadata $configurableOptionsMetadata + * @param SelectionUidFormatter $selectionUidFormatter + * @param Variant $variant + * @param VariantFormatter $variantFormatter + * @param Data $configurableProductHelper */ public function __construct( - Metadata $configurableSelectionMetadata + Metadata $configurableSelectionMetadata, + ConfigurableOptionsMetadata $configurableOptionsMetadata, + SelectionUidFormatter $selectionUidFormatter, + Variant $variant, + VariantFormatter $variantFormatter, + Data $configurableProductHelper ) { $this->configurableSelectionMetadata = $configurableSelectionMetadata; + $this->configurableOptionsMetadata = $configurableOptionsMetadata; + $this->selectionUidFormatter = $selectionUidFormatter; + $this->variant = $variant; + $this->variantFormatter = $variantFormatter; + $this->configurableProductHelper = $configurableProductHelper; } /** @@ -40,10 +86,31 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value throw new LocalizedException(__('"model" value should be specified')); } - $selectedOptions = $args['configurableOptionValueUids'] ?? []; - /** @var ProductInterface $product */ $product = $value['model']; - return $this->configurableSelectionMetadata->getAvailableSelections($product, $selectedOptions); + $selectionUids = $args['configurableOptionValueUids'] ?? []; + $selectedOptions = $this->selectionUidFormatter->extract($selectionUids); + + $variants = $this->variant->getSalableVariantsByParent($product); + $options = $this->configurableProductHelper->getOptions($product, $variants); + + $configurableOptions = $this->configurableOptionsMetadata->getAvailableSelections( + $product, + $options, + $selectedOptions + ); + + $optionsAvailableForSelection = $this->configurableSelectionMetadata->getAvailableSelections( + $product, + $args['configurableOptionValueUids'] ?? [] + ); + + return [ + 'configurable_options' => $configurableOptions, + 'variant' => $this->variantFormatter->format($options, $selectedOptions, $variants), + 'model' => $product, + 'options_available_for_selection' => $optionsAvailableForSelection['options_available_for_selection'], + 'availableSelectionProducts' => $optionsAvailableForSelection['availableSelectionProducts'] + ]; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php index 4aa322e66df60..972e4a9fd629a 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php @@ -21,11 +21,11 @@ class SelectionMediaGallery implements ResolverInterface */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (!isset($value['product']) || !$value['product']) { + if (!isset($value['model']) || !$value['model']) { return null; } - $product = $value['product']; + $product = $value['model']; $availableSelectionProducts = $value['availableSelectionProducts']; $mediaGalleryEntries = []; $usedProducts = $product->getTypeInstance()->getUsedProducts($product, null); diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 3e84a428765fc..d9ba786c84354 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -7,7 +7,7 @@ type Mutation { type ConfigurableProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "ConfigurableProduct defines basic features of a configurable product and its simple product variants") { variants: [ConfigurableVariant] @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") configurable_options: [ConfigurableProductOptions] @doc(description: "An array of linked simple product items") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Options") - configurable_product_options_selection(configurableOptionValueUids: [ID!]): ConfigurableProductOptionsSelection @doc(description: "Metadata for the specified configurable options selection") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\OptionsSelectionMetadata") + configurable_product_options_selection(configurableOptionValueUids: [ID!]): ConfigurableProductOptionsSelection @doc(description: "Specified configurable product options selection") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\OptionsSelectionMetadata") } type ConfigurableVariant @doc(description: "An array containing all the simple product variants of a configurable product") { @@ -83,6 +83,7 @@ type ConfigurableWishlistItem implements WishlistItemInterface @doc(description: type ConfigurableProductOptionsSelection @doc(description: "Metadata corresponding to the configurable options selection.") { options_available_for_selection: [ConfigurableOptionAvailableForSelection!] @doc(description: "Configurable options available for further selection based on current selection.") + configurable_options: [ConfigurableProductOption!] @doc(description: "Configurable options available for further selection based on current selection.") media_gallery: [MediaGalleryInterface!] @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\SelectionMediaGallery") @doc(description: "Product images and videos corresponding to the specified configurable options selection.") variant: SimpleProduct @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Variant") @doc(description: "Variant represented by the specified configurable options selection. It is expected to be null, until selections are made for each configurable option.") } @@ -92,6 +93,20 @@ type ConfigurableOptionAvailableForSelection @doc(description: "Configurable opt attribute_code: String! @doc(description: "Attribute code that uniquely identifies configurable option.") } +type ConfigurableProductOption { + uid: ID! + attribute_code: String! + label: String! + values: [ConfigurableProductOptionValue!] +} + +type ConfigurableProductOptionValue { + uid: ID! + is_available: Boolean! + is_use_default: Boolean! + label: String! +} + type StoreConfig @doc(description: "The type contains information about a store config") { configurable_thumbnail_source : String @doc(description: "The configuration setting determines which thumbnail should be used in the cart for configurable products.") } diff --git a/app/code/Magento/Downloadable/view/frontend/templates/catalog/product/type.phtml b/app/code/Magento/Downloadable/view/frontend/templates/catalog/product/type.phtml index ba6d9e0abec71..e5397e758d63f 100644 --- a/app/code/Magento/Downloadable/view/frontend/templates/catalog/product/type.phtml +++ b/app/code/Magento/Downloadable/view/frontend/templates/catalog/product/type.phtml @@ -12,7 +12,7 @@ ?> <?php $_product = $block->getProduct() ?> -<?php if ($_product->getIsSalable()) : ?> +<?php if ($_product->isAvailable()) : ?> <div class="stock available" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> <span><?= $block->escapeHtml(__('In stock')) ?></span> </div> diff --git a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php index 05e2e836abada..e0f5509830a0c 100644 --- a/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Helper/File/Storage/Database.php @@ -14,6 +14,9 @@ * * @api * @since 100.0.2 + * + * @deprecated Database Media Storage is deprecated + * */ class Database extends \Magento\Framework\App\Helper\AbstractHelper { diff --git a/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php b/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php index 1a55be299a75b..81e6c5e028be1 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php +++ b/app/code/Magento/MediaStorage/Model/Config/Backend/Storage/Media/Database.php @@ -4,7 +4,9 @@ * See COPYING.txt for license details. */ namespace Magento\MediaStorage\Model\Config\Backend\Storage\Media; - +/** +* @deprecated Database Media Storage is deprecated +**/ class Database extends \Magento\Framework\App\Config\Value { /** diff --git a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php index 83134c2ac00ef..97f161e4c49d8 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php +++ b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Database.php @@ -12,6 +12,9 @@ use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Config\ConfigOptionsListConstants; +/** + * @deprecated Database Media Storage is deprecated + **/ class Database implements \Magento\Framework\Option\ArrayInterface { /** diff --git a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php index fb171831407e2..c881b59c5c593 100644 --- a/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php +++ b/app/code/Magento/MediaStorage/Model/Config/Source/Storage/Media/Storage.php @@ -23,7 +23,7 @@ public function toOptionArray() 'value' => \Magento\MediaStorage\Model\File\Storage::STORAGE_MEDIA_FILE_SYSTEM, 'label' => __('File System'), ], - ['value' => \Magento\MediaStorage\Model\File\Storage::STORAGE_MEDIA_DATABASE, 'label' => __('Database')] + ['value' => \Magento\MediaStorage\Model\File\Storage::STORAGE_MEDIA_DATABASE, 'label' => __('Database (Deprecated)')] ]; } } diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Database.php b/app/code/Magento/MediaStorage/Model/File/Storage/Database.php index 2bdc69f45ccb8..571dad7f0ae9a 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Database.php @@ -10,6 +10,8 @@ * * @api * @since 100.0.2 + * + * @deprecated Database Media Storage is deprecated */ class Database extends \Magento\MediaStorage\Model\File\Storage\Database\AbstractDatabase { diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php b/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php index c9812b86e8b91..3528ca5743ff7 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Database/AbstractDatabase.php @@ -7,7 +7,10 @@ /** * Class AbstractDatabase - */ + * + * @deprecated Database Media Storage is deprecated + * + **/ abstract class AbstractDatabase extends \Magento\Framework\Model\AbstractModel { /** diff --git a/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php b/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php index 2617e88e7538a..03d5eee617b1b 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage/Directory/Database.php @@ -11,6 +11,8 @@ * * @api * @since 100.0.2 + * + * @deprecated Database Media Storage is deprecated */ class Database extends \Magento\MediaStorage\Model\File\Storage\Database\AbstractDatabase { diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php index 227b428328b79..0533c0229ea3d 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/AbstractStorage.php @@ -7,6 +7,8 @@ /** * Class AbstractStorage + * + * @deprecated Database Media Storage is deprecated */ abstract class AbstractStorage extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php index ae896395b8eb5..863b368883fbe 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Database.php @@ -10,6 +10,8 @@ * * @api * @since 100.0.2 + * + * @deprecated Database Media Storage is deprecated */ class Database extends \Magento\MediaStorage\Model\ResourceModel\File\Storage\AbstractStorage { diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php index e5f54cac4af6a..342761646e396 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/Directory/Database.php @@ -7,6 +7,8 @@ /** * Class Database + * + * @deprecated Database Media Storage is deprecated */ class Database extends \Magento\MediaStorage\Model\ResourceModel\File\Storage\AbstractStorage { diff --git a/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml b/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml index 562c9a2b63a99..da11582a16133 100644 --- a/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml @@ -60,7 +60,7 @@ $rating = 'short'; </button> <?php endif; ?> <?php else : ?> - <?php if ($item->getIsSalable()) : ?> + <?php if ($item->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml index a54259280e381..a9d5718449cd5 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml @@ -78,7 +78,7 @@ if ($exist = $block->getRecentlyComparedProducts()) { <?php endif; ?> </div> <?php else : ?> - <?php if ($_product->getIsSalable()) : ?> + <?php if ($_product->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml index ad6b33820c752..1222490065185 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml @@ -83,7 +83,7 @@ if ($exist = $block->getRecentlyComparedProducts()) { </button> <?php endif; ?> <?php else : ?> - <?php if ($_item->getIsSalable()) : ?> + <?php if ($_item->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml index ba7a50eef6485..6f7b4f4f66f27 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml @@ -84,7 +84,7 @@ if ($exist = $block->getRecentlyComparedProducts()) { </button> <?php endif; ?> <?php else : ?> - <?php if ($_item->getIsSalable()) : ?> + <?php if ($_item->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml index 16fc2b070b95c..3e5cd15bbc62b 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml @@ -81,7 +81,7 @@ if ($exist = ($block->getRecentlyViewedProducts() && $block->getRecentlyViewedPr <?php endif; ?> </div> <?php else : ?> - <?php if ($_product->getIsSalable()) : ?> + <?php if ($_product->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml index 567c3ebc57f9b..c2f98e72909d6 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml @@ -86,7 +86,7 @@ if ($exist = ($block->getRecentlyViewedProducts() && $block->getRecentlyViewedPr </button> <?php endif; ?> <?php else : ?> - <?php if ($_item->getIsSalable()) : ?> + <?php if ($_item->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml index 9a8bb9c3b734f..32cf0bc69d1e5 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml @@ -88,7 +88,7 @@ if ($exist = ($block->getRecentlyViewedProducts() && $block->getRecentlyViewedPr </button> <?php endif; ?> <?php else : ?> - <?php if ($_item->getIsSalable()) : ?> + <?php if ($_item->isAvailable()) : ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> <?php else : ?> <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php index c2238ff1a3809..f88fecf84be6a 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php @@ -654,7 +654,7 @@ public function getMethodPrice($cost, $method = '') 'free_shipping_enable' ) && $this->getConfigData( 'free_shipping_subtotal' - ) <= $this->_rawRequest->getBaseSubtotalInclTax() ? '0.00' : $this->getFinalPriceWithHandlingFee( + ) <= $this->_rawRequest->getValueWithDiscount() ? '0.00' : $this->getFinalPriceWithHandlingFee( $cost ); } diff --git a/app/code/Magento/SwatchesGraphQl/composer.json b/app/code/Magento/SwatchesGraphQl/composer.json index 1b98b4044a2ff..959f0f201d2b3 100644 --- a/app/code/Magento/SwatchesGraphQl/composer.json +++ b/app/code/Magento/SwatchesGraphQl/composer.json @@ -9,6 +9,9 @@ "magento/module-catalog": "*", "magento/module-catalog-graph-ql": "*" }, + "suggest": { + "magento/module-configurable-product-graph-ql": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/SwatchesGraphQl/etc/module.xml b/app/code/Magento/SwatchesGraphQl/etc/module.xml index 6689f13db754e..71c336a8cd257 100644 --- a/app/code/Magento/SwatchesGraphQl/etc/module.xml +++ b/app/code/Magento/SwatchesGraphQl/etc/module.xml @@ -10,6 +10,7 @@ <sequence> <module name="Magento_Catalog"/> <module name="Magento_Swatches"/> + <module name="Magento_ConfigurableProductGraphQl"/> </sequence> </module> </config> diff --git a/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls b/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls index c51468ccd2856..3491568108daf 100644 --- a/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls @@ -47,3 +47,7 @@ type TextSwatchData implements SwatchDataInterface { type ColorSwatchData implements SwatchDataInterface { } + +type ConfigurableProductOptionValue { + swatch: SwatchDataInterface @resolver(class: "Magento\\SwatchesGraphQl\\Model\\Resolver\\Product\\Options\\SwatchData") +} diff --git a/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php b/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php index daff459d7ba3f..07da63fa8476e 100644 --- a/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php +++ b/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php @@ -226,7 +226,7 @@ public function testGetMethodPrice( ->willReturn($freeShippingEnabled); $request = new RateRequest(); - $request->setBaseSubtotalInclTax($requestSubtotal); + $request->setValueWithDiscount($requestSubtotal); $this->model->setRawRequest($request); $price = $this->model->getMethodPrice($cost, $shippingMethod); $this->assertEquals($expectedPrice, $price); diff --git a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less index 3e9f2d4401b05..d88260e01b25d 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less +++ b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less @@ -66,6 +66,10 @@ display: inline-block; overflow: hidden; width: 100%; + + &.white-space-preserved { + white-space: pre; + } } body._in-resize { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php index 659720578de10..5811344e4defd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php @@ -3,18 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\GraphQl\ConfigurableProduct; use Exception; +use Magento\CatalogInventory\Model\Configuration; use Magento\Config\Model\ResourceModel\Config; +use Magento\ConfigurableProductGraphQl\Model\Options\SelectionUidFormatter; use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\CatalogInventory\Model\Configuration; /** * Add configurable product to cart testcases @@ -41,6 +43,11 @@ class AddConfigurableProductToCartSingleMutationTest extends GraphQlAbstract */ private $reinitConfig; + /** + * @var SelectionUidFormatter + */ + private $selectionUidFormatter; + /** * @inheritdoc */ @@ -51,13 +58,14 @@ protected function setUp(): void $this->resourceConfig = $objectManager->get(Config::class); $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); + $this->selectionUidFormatter = $objectManager->get(SelectionUidFormatter::class); } /** * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php */ - public function testAddConfigurableProductToCart() + public function testAddConfigurableProductToCart(): void { $product = $this->getConfigurableProductInfo(); $quantity = 2; @@ -77,7 +85,8 @@ public function testAddConfigurableProductToCart() ); $response = $this->graphQlMutation($query); - $expectedProductOptionsValueUid = $this->generateConfigurableSelectionUID($attributeId, $valueIndex); + + $expectedProductOptionsValueUid = $this->selectionUidFormatter->encode($attributeId, $valueIndex); $expectedProductOptionsUid = base64_encode("configurable/$productRowId/$attributeId"); $cartItem = current($response['addProductsToCart']['cart']['items']); self::assertEquals($quantity, $cartItem['quantity']); @@ -94,35 +103,11 @@ public function testAddConfigurableProductToCart() self::assertArrayHasKey('value_label', $option); } - /** - * Generates UID configurable product - * - * @param int $attributeId - * @param int $valueIndex - * @return string - */ - private function generateConfigurableSelectionUID(int $attributeId, int $valueIndex): string - { - return base64_encode("configurable/$attributeId/$valueIndex"); - } - - /** - * Generates UID for super configurable product super attributes - * - * @param int $attributeId - * @param int $valueIndex - * @return string - */ - private function generateSuperAttributesUIDQuery(int $attributeId, int $valueIndex): string - { - return 'selected_options: ["' . $this->generateConfigurableSelectionUID($attributeId, $valueIndex) . '"]'; - } - /** * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php */ - public function testAddConfigurableProductWithWrongSuperAttributes() + public function testAddConfigurableProductWithWrongSuperAttributes(): void { $product = $this->getConfigurableProductInfo(); $quantity = 2; @@ -150,7 +135,7 @@ public function testAddConfigurableProductWithWrongSuperAttributes() * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php */ - public function testAddProductIfQuantityIsNotAvailable() + public function testAddProductIfQuantityIsNotAvailable(): void { $product = $this->getConfigurableProductInfo(); $parentSku = $product['sku']; @@ -179,7 +164,7 @@ public function testAddProductIfQuantityIsNotAvailable() * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php */ - public function testAddNonExistentConfigurableProductParentToCart() + public function testAddNonExistentConfigurableProductParentToCart(): void { $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); $parentSku = 'configurable_no_exist'; @@ -203,7 +188,7 @@ public function testAddNonExistentConfigurableProductParentToCart() * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_zero_qty_first_child.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php */ - public function testOutOfStockVariationToCart() + public function testOutOfStockVariationToCart(): void { $showOutOfStock = $this->scopeConfig->getValue(Configuration::XML_PATH_SHOW_OUT_OF_STOCK); @@ -215,7 +200,7 @@ public function testOutOfStockVariationToCart() $attributeId = (int) $product['configurable_options'][0]['attribute_id']; $valueIndex = $product['configurable_options'][0]['values'][0]['value_index']; // Asserting that the first value is the right option we want to add to cart - $this->assertEquals( + self::assertEquals( $product['configurable_options'][0]['values'][0]['label'], 'Option 1' ); @@ -237,7 +222,7 @@ public function testOutOfStockVariationToCart() 'There are no source items with the in stock status', 'This product is out of stock.' ]; - $this->assertContains( + self::assertContains( $response['addProductsToCart']['user_errors'][0]['message'], $expectedErrorMessages ); @@ -312,6 +297,18 @@ private function getConfigurableProductInfo(): array return current($searchResponse['products']['items']); } + /** + * Generates UID for super configurable product super attributes + * + * @param int $attributeId + * @param int $valueIndex + * @return string + */ + private function generateSuperAttributesUIDQuery(int $attributeId, int $valueIndex): string + { + return 'selected_options: ["' . $this->selectionUidFormatter->encode($attributeId, $valueIndex) . '"]'; + } + /** * Returns GraphQl query for fetching configurable product information * @@ -354,10 +351,27 @@ private function getFetchProductQuery(string $term): string attribute_code option_value_uids } + configurable_options { + uid + attribute_code + label + values { + uid + is_available + is_use_default + label + } + } variant { uid - name - attribute_set_id + sku + url_key + url_path + } + media_gallery { + url + label + disabled } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php new file mode 100644 index 0000000000000..ac252acfcaa2b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php @@ -0,0 +1,379 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Magento\ConfigurableProductGraphQl\Model\Options\SelectionUidFormatter; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\AttributeRepository; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Query\Uid; +use Magento\Indexer\Model\IndexerFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test configurable product option selection. + */ +class ConfigurableOptionsSelectionTest extends GraphQlAbstract +{ + /** + * @var AttributeRepository + */ + private $attributeRepository; + + /** + * @var SelectionUidFormatter + */ + private $selectionUidFormatter; + + /** + * @var IndexerFactory + */ + private $indexerFactory; + + /** + * @var Uid + */ + private $idEncoder; + + private $firstConfigurableAttribute; + + private $secondConfigurableAttribute; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepository::class); + $this->selectionUidFormatter = Bootstrap::getObjectManager()->create(SelectionUidFormatter::class); + $this->indexerFactory = Bootstrap::getObjectManager()->create(IndexerFactory::class); + $this->idEncoder = Bootstrap::getObjectManager()->create(Uid::class); + } + + /** + * Test the first option of the first attribute selected + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testSelectedFirstAttributeFirstOption(): void + { + $attribute = $this->getFirstConfigurableAttribute(); + $options = $attribute->getOptions(); + $sku = 'configurable_12345'; + $firstOptionUid = $this->selectionUidFormatter->encode( + (int)$attribute->getAttributeId(), + (int)$options[1]->getValue() + ); + + $this->reindexAll(); + $response = $this->graphQlQuery($this->getQuery($sku, [$firstOptionUid])); + + self::assertNotEmpty($response['products']['items']); + $product = current($response['products']['items']); + self::assertEquals('ConfigurableProduct', $product['__typename']); + self::assertEquals($sku, $product['sku']); + self::assertNotEmpty($product['configurable_product_options_selection']['configurable_options']); + self::assertNull($product['configurable_product_options_selection']['variant']); + self::assertCount(1, $product['configurable_product_options_selection']['configurable_options']); + self::assertCount(4, $product['configurable_product_options_selection']['configurable_options'][0]['values']); + + $secondAttributeOptions = $this->getSecondConfigurableAttribute()->getOptions(); + $this->assertAvailableOptionUids( + $this->getSecondConfigurableAttribute()->getAttributeId(), + $secondAttributeOptions, + $this->getOptionsUids( + $product['configurable_product_options_selection']['configurable_options'][0]['values'] + ) + ); + + $this->assertMediaGallery($product); + } + + /** + * Test selected variant + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testSelectedVariant(): void + { + $firstAttribute = $this->getFirstConfigurableAttribute(); + $firstOptions = $firstAttribute->getOptions(); + $firstAttributeFirstOptionUid = $this->selectionUidFormatter->encode( + (int)$firstAttribute->getAttributeId(), + (int)$firstOptions[1]->getValue() + ); + $secondAttribute = $this->getSecondConfigurableAttribute(); + $secondOptions = $secondAttribute->getOptions(); + $secondAttributeFirstOptionUid = $this->selectionUidFormatter->encode( + (int)$secondAttribute->getAttributeId(), + (int)$secondOptions[1]->getValue() + ); + + $sku = 'configurable_12345'; + + $this->reindexAll(); + $response = $this->graphQlQuery( + $this->getQuery($sku, [$firstAttributeFirstOptionUid, $secondAttributeFirstOptionUid]) + ); + + self::assertNotEmpty($response['products']['items']); + $product = current($response['products']['items']); + self::assertEquals('ConfigurableProduct', $product['__typename']); + self::assertEquals($sku, $product['sku']); + self::assertEmpty($product['configurable_product_options_selection']['configurable_options']); + self::assertNotNull($product['configurable_product_options_selection']['variant']); + + $variantId = $this->idEncoder->decode($product['configurable_product_options_selection']['variant']['uid']); + self::assertIsNumeric($variantId); + self::assertIsString($product['configurable_product_options_selection']['variant']['sku']); + $urlKey = 'configurable-option-first-option-1-second-option-1'; + self::assertEquals($urlKey, $product['configurable_product_options_selection']['variant']['url_key']); + + $this->assertMediaGallery($product); + } + + /** + * Test without selected options + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testWithoutSelectedOption(): void + { + $sku = 'configurable_12345'; + $this->reindexAll(); + $response = $this->graphQlQuery($this->getQuery($sku)); + + self::assertNotEmpty($response['products']['items']); + $product = current($response['products']['items']); + self::assertEquals('ConfigurableProduct', $product['__typename']); + self::assertEquals($sku, $product['sku']); + + self::assertNotEmpty($product['configurable_product_options_selection']['configurable_options']); + self::assertNull($product['configurable_product_options_selection']['variant']); + self::assertCount(2, $product['configurable_product_options_selection']['configurable_options']); + self::assertCount(4, $product['configurable_product_options_selection']['configurable_options'][0]['values']); + self::assertCount(4, $product['configurable_product_options_selection']['configurable_options'][1]['values']); + + $firstAttributeOptions = $this->getFirstConfigurableAttribute()->getOptions(); + $this->assertAvailableOptionUids( + $this->getFirstConfigurableAttribute()->getAttributeId(), + $firstAttributeOptions, + $this->getOptionsUids( + $product['configurable_product_options_selection']['configurable_options'][0]['values'] + ) + ); + + $secondAttributeOptions = $this->getSecondConfigurableAttribute()->getOptions(); + $this->assertAvailableOptionUids( + $this->getSecondConfigurableAttribute()->getAttributeId(), + $secondAttributeOptions, + $this->getOptionsUids( + $product['configurable_product_options_selection']['configurable_options'][1]['values'] + ) + ); + + $this->assertMediaGallery($product); + } + + /** + * Test with wrong selected options + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testWithWrongSelectedOptions(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('configurableOptionValueUids values are incorrect'); + + $attribute = $this->getFirstConfigurableAttribute(); + $options = $attribute->getOptions(); + $sku = 'configurable_12345'; + $firstOptionUid = $this->selectionUidFormatter->encode( + (int)$attribute->getAttributeId(), + $options[1]->getValue() + 100 + ); + + $this->reindexAll(); + $this->graphQlQuery($this->getQuery($sku, [$firstOptionUid])); + } + + /** + * Get GraphQL query to test configurable product options selection + * + * @param string $productSku + * @param array $optionValueUids + * @param int $pageSize + * @param int $currentPage + * @return string + */ + private function getQuery( + string $productSku, + array $optionValueUids = [], + int $pageSize = 20, + int $currentPage = 1 + ): string { + if (empty($optionValueUids)) { + $configurableOptionValueUids = ''; + } else { + $configurableOptionValueUids = '(configurableOptionValueUids: ['; + foreach ($optionValueUids as $configurableOptionValueUid) { + $configurableOptionValueUids .= '"' . $configurableOptionValueUid . '",'; + } + $configurableOptionValueUids .= '])'; + } + + return <<<QUERY +{ +products(filter:{ + sku: {eq: "{$productSku}"} + }, + pageSize: {$pageSize}, currentPage: {$currentPage} + ) + { + items { + __typename + sku + ... on ConfigurableProduct { + configurable_product_options_selection {$configurableOptionValueUids} { + configurable_options { + uid + attribute_code + label + values { + uid + is_available + is_use_default + label + swatch { + value + } + } + } + variant { + uid + sku + url_key + } + media_gallery { + url + label + disabled + } + } + } + } + } +} +QUERY; + } + + /** + * Get first configurable attribute. + * + * @return AttributeInterface + * @throws NoSuchEntityException + */ + private function getFirstConfigurableAttribute(): AttributeInterface + { + if (!$this->firstConfigurableAttribute) { + $this->firstConfigurableAttribute = $this->attributeRepository->get( + 'catalog_product', + 'test_configurable_first' + ); + } + + return $this->firstConfigurableAttribute; + } + + /** + * Get second configurable attribute. + * + * @return AttributeInterface + * @throws NoSuchEntityException + */ + private function getSecondConfigurableAttribute(): AttributeInterface + { + if (!$this->secondConfigurableAttribute) { + $this->secondConfigurableAttribute = $this->attributeRepository->get( + 'catalog_product', + 'test_configurable_second' + ); + } + + return $this->secondConfigurableAttribute; + } + + /** + * Assert option uid. + * + * @param $attributeId + * @param $expectedOptions + * @param $selectedOptions + */ + private function assertAvailableOptionUids($attributeId, $expectedOptions, $selectedOptions): void + { + unset($expectedOptions[0]); + foreach ($expectedOptions as $option) { + self::assertContains( + $this->selectionUidFormatter->encode((int)$attributeId, (int)$option->getValue()), + $selectedOptions + ); + } + } + + /** + * Make fulltext catalog search reindex + * + * @return void + * @throws \Throwable + */ + private function reindexAll(): void + { + $indexLists = [ + 'catalog_category_product', + 'catalog_product_attribute', + 'cataloginventory_stock', + 'catalogsearch_fulltext', + ]; + + foreach ($indexLists as $indexerId) { + $indexer = $this->indexerFactory->create(); + $indexer->load($indexerId)->reindexAll(); + } + } + + /** + * Retrieve options UIDs + * + * @param array $options + * @return array + */ + private function getOptionsUids(array $options): array + { + $uids = []; + foreach ($options as $option) { + $uids[] = $option['uid']; + } + return $uids; + } + + /** + * Assert media gallery fields + * + * @param array $product + */ + private function assertMediaGallery(array $product): void + { + self::assertNotEmpty($product['configurable_product_options_selection']['media_gallery']); + $image = current($product['configurable_product_options_selection']['media_gallery']); + self::assertIsString($image['url']); + self::assertEquals(false, $image['disabled']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php index ae34ea31f0d51..713a16a6bfaa9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php @@ -84,12 +84,12 @@ public function testVisualSwatchDataValues() $color = '#000000'; $query = <<<QUERY { - products(filter: {sku: {eq: "$productSku"}}) { + products(filter: { sku: { eq: "$productSku" } }) { items { - ... on ConfigurableProduct{ - configurable_options{ + ... on ConfigurableProduct { + configurable_options { values { - swatch_data{ + swatch_data { value ... on ImageSwatchData { thumbnail @@ -97,6 +97,19 @@ public function testVisualSwatchDataValues() } } } + configurable_product_options_selection { + configurable_options { + values { + label + swatch { + value + ... on ImageSwatchData { + thumbnail + } + } + } + } + } } } } @@ -123,5 +136,19 @@ public function testVisualSwatchDataValues() $option['values'][1]['swatch_data']['thumbnail'], $this->swatchMediaHelper->getSwatchAttributeImage(Swatch::SWATCH_THUMBNAIL_NAME, $imageName) ); + + $configurableProductOptionsSelection = + $product['configurable_product_options_selection']['configurable_options'][0]; + + $this->assertArrayHasKey('values', $configurableProductOptionsSelection); + $this->assertEquals($color, $configurableProductOptionsSelection['values'][0]['swatch']['value']); + $this->assertStringContainsString( + $configurableProductOptionsSelection['values'][1]['swatch']['value'], + $this->swatchMediaHelper->getSwatchAttributeImage(Swatch::SWATCH_IMAGE_NAME, $imageName) + ); + $this->assertEquals( + $configurableProductOptionsSelection['values'][1]['swatch']['thumbnail'], + $this->swatchMediaHelper->getSwatchAttributeImage(Swatch::SWATCH_THUMBNAIL_NAME, $imageName) + ); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php index 00c3133c25439..36bca76b28de1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php @@ -8,14 +8,18 @@ namespace Magento\Catalog\Controller; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; use Magento\Catalog\Model\Product\ProductList\Toolbar as ToolbarModel; +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\Catalog\Model\Session; use Magento\Framework\App\Http\Context; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; use Magento\Framework\View\LayoutInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\AbstractController; @@ -53,6 +57,11 @@ class CategoryTest extends AbstractController */ private $httpContext; + /** + * @var CollectionFactory + */ + private $categoryCollectionFactory; + /** * @inheritdoc */ @@ -64,6 +73,8 @@ protected function setUp(): void $this->objectManager->configure([ 'preferences' => [LayoutUpdateManager::class => CategoryLayoutUpdateManager::class] ]); + + $this->categoryCollectionFactory = $this->objectManager->create(CollectionFactory::class); $this->registry = $this->objectManager->get(Registry::class); $this->layout = $this->objectManager->get(LayoutInterface::class); $this->session = $this->objectManager->get(Session::class); @@ -233,4 +244,50 @@ public function testViewWithRememberPaginationAndPreviousValue(): void $this->assertEquals($newPaginationValue, $this->session->getData(ToolbarModel::LIMIT_PARAM_NAME)); $this->assertEquals($newPaginationValue, $this->httpContext->getValue(ToolbarModel::LIMIT_PARAM_NAME)); } + + /** + * Test to generate category page without duplicate html element ids + * + * @magentoDataFixture Magento/Catalog/_files/category_with_three_products.php + * @magentoDataFixture Magento/Catalog/_files/catalog_category_product_reindex_all.php + * @magentoDataFixture Magento/Catalog/_files/catalog_product_category_reindex_all.php + * @magentoDbIsolation disabled + */ + public function testViewWithoutDuplicateHmlElementIds(): void + { + $category = $this->loadCategory('Category 999', Store::DEFAULT_STORE_ID); + $this->dispatch('catalog/category/view/id/' . $category->getId()); + + $responseHtml = $this->getResponse()->getBody(); + $htmlElementIds = ['modes-label', 'mode-list', 'toolbar-amount', 'sorter', 'limiter']; + foreach ($htmlElementIds as $elementId) { + $matches = []; + $idAttribute = "id=\"$elementId\""; + preg_match_all("/$idAttribute/mx", $responseHtml, $matches); + $this->assertCount(1, $matches[0]); + $this->assertEquals($idAttribute, $matches[0][0]); + } + } + + /** + * Loads category by id + * + * @param string $categoryName + * @param int $storeId + * @return CategoryInterface + */ + private function loadCategory(string $categoryName, int $storeId): CategoryInterface + { + /** @var Collection $categoryCollection */ + $categoryCollection = $this->categoryCollectionFactory->create(); + /** @var CategoryInterface $category */ + $category = $categoryCollection->setStoreId($storeId) + ->addAttributeToSelect('display_mode', 'left') + ->addAttributeToFilter(CategoryInterface::KEY_NAME, $categoryName) + ->setPageSize(1) + ->getFirstItem(); + $category->setStoreId($storeId); + + return $category; + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php index 24e6010275bac..f26e39ca8e2a2 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexerProcessor; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Media\Config; use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Setup\CategorySetup; @@ -15,8 +17,11 @@ use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Framework\App\Filesystem\DirectoryList; Resolver::getInstance()->requireDataFixture( 'Magento/ConfigurableProduct/_files/configurable_attribute_first.php' @@ -25,18 +30,31 @@ 'Magento/ConfigurableProduct/_files/configurable_attribute_second.php' ); +$objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ -$productRepository = Bootstrap::getObjectManager() - ->get(ProductRepositoryInterface::class); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); /** @var $installer CategorySetup */ -$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); +$installer = $objectManager->create(CategorySetup::class); /** @var \Magento\Eav\Model\Config $eavConfig */ -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$eavConfig = $objectManager->get(\Magento\Eav\Model\Config::class); $firstAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_first'); $secondAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_second'); +/** @var Config $config */ +$config = $objectManager->get(Config::class); + +/** @var Filesystem $filesystem */ +$filesystem = $objectManager->get(Filesystem::class); + +/** @var WriteInterface $mediaDirectory */ +$mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); +$mediaPath = $mediaDirectory->getAbsolutePath(); +$baseTmpMediaPath = $config->getBaseTmpMediaPath(); +$mediaDirectory->create($baseTmpMediaPath); + /* Create simple products per each option value*/ /** @var AttributeOptionInterface[] $firstAttributeOptions */ $firstAttributeOptions = $firstAttribute->getOptions(); @@ -48,6 +66,8 @@ $firstAttributeValues = []; $secondAttributeValues = []; $testImagePath = __DIR__ . '/magento_image.jpg'; +$mediaImage = $mediaPath . '/' . $baseTmpMediaPath . '/magento_image.jpg'; +copy($testImagePath, $mediaImage); array_shift($firstAttributeOptions); array_shift($secondAttributeOptions); @@ -65,7 +85,10 @@ $qty = 100; $isInStock = 1; } - $product = Bootstrap::getObjectManager()->create(Product::class); + + $image = '/m/a/magento_image.jpg'; + + $product = $objectManager->create(Product::class); $product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($attributeSetId) ->setWebsiteIds([1]) @@ -79,15 +102,15 @@ ->setStockData( ['use_config_manage_stock' => 1, 'qty' => $qty, 'is_qty_decimal' => 0, 'is_in_stock' => $isInStock] ) - ->setImage('/m/a/magento_image.jpg') - ->setSmallImage('/m/a/magento_image.jpg') - ->setThumbnail('/m/a/magento_image.jpg') + ->setImage($image) + ->setSmallImage($image) + ->setThumbnail($image) ->setData( 'media_gallery', [ 'images' => [ [ - 'file' => '/m/a/magento_image.jpg', + 'file' => $image, 'position' => 1, 'label' => 'Image Alt Text', 'disabled' => 0, @@ -113,11 +136,12 @@ foreach ($customAttributes as $attributeCode => $attributeValue) { $product->setCustomAttributes($customAttributes); } + $product = $productRepository->save($product); $associatedProductIds[] = $product->getId(); - /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ - $stockItem = Bootstrap::getObjectManager()->create(Item::class); + /** @var Item $stockItem */ + $stockItem = $objectManager->create(Item::class); $stockItem->load($product->getId(), 'product_id'); if (!$stockItem->getProductId()) { @@ -135,17 +159,16 @@ 'value_index' => $secondAttributeOption->getValue(), ]; } - } -$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); +$indexerProcessor = $objectManager->get(PriceIndexerProcessor::class); $indexerProcessor->reindexList($associatedProductIds, true); /** @var $product Product */ -$product = Bootstrap::getObjectManager()->create(Product::class); +$product = $objectManager->create(Product::class); /** @var Factory $optionsFactory */ -$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); +$optionsFactory = $objectManager->create(Factory::class); $configurableAttributesData = [ [ @@ -180,9 +203,15 @@ ->setSku('configurable_12345') ->setVisibility(Visibility::VISIBILITY_BOTH) ->setStatus(Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->addImageToMediaGallery( + $mediaImage, + ['image', 'small_image', 'thumbnail'], + false, + false + ); $productRepository->cleanCache(); $product = $productRepository->save($product); -$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); +$indexerProcessor = $objectManager->get(PriceIndexerProcessor::class); $indexerProcessor->reindexRow($product->getId(), true); diff --git a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php index 552040489e253..57c6042ca25c9 100644 --- a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php @@ -442,17 +442,8 @@ private function getExpectedLabelRequestXml( */ public function testCollectRates() { - $requestData = $this->getRequestData(); - //phpcs:disable Magento2.Functions.DiscouragedFunction - $response = new Response( - 200, - [], - file_get_contents(__DIR__ . '/../_files/dhl_quote_response.xml') - ); - //phpcs:enable Magento2.Functions.DiscouragedFunction - $this->httpClient->nextResponses(array_fill(0, Carrier::UNAVAILABLE_DATE_LOOK_FORWARD + 1, $response)); - /** @var RateRequest $request */ - $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); + $this->setNextResponse(__DIR__ . '/../_files/dhl_quote_response.xml'); + $request = $this->createRequest(); $expectedRates = [ ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 45.85, 'method' => 'E', 'price' => 45.85], ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 35.26, 'method' => 'Q', 'price' => 35.26], @@ -487,11 +478,9 @@ public function testCollectRates() */ public function testCollectRatesWithoutDimensions(?string $size, ?string $height, ?string $width, ?string $depth) { - $requestData = $this->getRequestData(); $this->setDhlConfig(['size' => $size, 'height' => $height, 'width' => $width, 'depth' => $depth]); - /** @var RateRequest $request */ - $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); + $request = $this->createRequest(); $this->dhlCarrier = Bootstrap::getObjectManager()->create(Carrier::class); $this->dhlCarrier->collectRates($request)->getAllRates(); @@ -511,15 +500,13 @@ public function testCollectRatesWithoutDimensions(?string $size, ?string $height public function testGetRatesWithHttpException(): void { $this->setDhlConfig(['showmethod' => 1]); - $requestData = $this->getRequestData(); $deferredResponse = $this->getMockBuilder(HttpResponseDeferredInterface::class) ->onlyMethods(['get']) ->getMockForAbstractClass(); $exception = new HttpException('Exception message'); $deferredResponse->method('get')->willThrowException($exception); $this->httpClient->setDeferredResponseMock($deferredResponse); - /** @var RateRequest $request */ - $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); + $request = $this->createRequest(); $this->dhlCarrier = Bootstrap::getObjectManager()->create(Carrier::class); $resultRate = $this->dhlCarrier->collectRates($request)->getAllRates()[0]; $error = Bootstrap::getObjectManager()->get(Error::class); @@ -563,6 +550,77 @@ private function setDhlConfig(array $params) } } + /** + * Tests that the free rate is returned when sending a quotes request + * + * @param array $addRequestData + * @param bool $freeShippingExpects + * @magentoConfigFixture default_store carriers/dhl/active 1 + * @magentoConfigFixture default_store carriers/dhl/id some ID + * @magentoConfigFixture default_store carriers/dhl/shipment_days Mon,Tue,Wed,Thu,Fri,Sat + * @magentoConfigFixture default_store carriers/dhl/intl_shipment_days Mon,Tue,Wed,Thu,Fri,Sat + * @magentoConfigFixture default_store carriers/dhl/allowed_methods IE + * @magentoConfigFixture default_store carriers/dhl/international_service IE + * @magentoConfigFixture default_store carriers/dhl/gateway_url https://xmlpi-ea.dhl.com/XMLShippingServlet + * @magentoConfigFixture default_store carriers/dhl/id some ID + * @magentoConfigFixture default_store carriers/dhl/password some password + * @magentoConfigFixture default_store carriers/dhl/content_type N + * @magentoConfigFixture default_store carriers/dhl/nondoc_methods 1,3,4,8,P,Q,E,F,H,J,M,V,Y + * @magentoConfigFixture default_store carriers/dhl/showmethod' => 1, + * @magentoConfigFixture default_store carriers/dhl/title DHL Title + * @magentoConfigFixture default_store carriers/dhl/specificerrmsg dhl error message + * @magentoConfigFixture default_store carriers/dhl/unit_of_measure K + * @magentoConfigFixture default_store carriers/dhl/size 1 + * @magentoConfigFixture default_store carriers/dhl/height 1.6 + * @magentoConfigFixture default_store carriers/dhl/width 1.6 + * @magentoConfigFixture default_store carriers/dhl/depth 1.6 + * @magentoConfigFixture default_store carriers/dhl/debug 1 + * @magentoConfigFixture default_store carriers/dhl/free_method_nondoc P + * @magentoConfigFixture default_store carriers/dhl/free_shipping_enable 1 + * @magentoConfigFixture default_store carriers/dhl/free_shipping_subtotal 25 + * @magentoConfigFixture default_store shipping/origin/country_id GB + * @magentoAppIsolation enabled + * @dataProvider collectRatesWithFreeShippingDataProvider + */ + public function testCollectRatesWithFreeShipping(array $addRequestData, bool $freeShippingExpects): void + { + $this->setNextResponse(__DIR__ . '/../_files/dhl_quote_response.xml'); + $request = $this->createRequest($addRequestData); + + $actualRates = $this->dhlCarrier->collectRates($request)->getAllRates(); + $freeRateExists = false; + foreach ($actualRates as $actualRate) { + $actualRate = $actualRate->getData(); + if ($actualRate['method'] === 'P' && (float)$actualRate['price'] === 0.0) { + $freeRateExists = true; + break; + } + } + + self::assertEquals($freeShippingExpects, $freeRateExists); + } + + /** + * @return array + */ + public function collectRatesWithFreeShippingDataProvider(): array + { + return [ + [ + ['package_value' => 25, 'package_value_with_discount' => 22], + false + ], + [ + ['package_value' => 25, 'package_value_with_discount' => 25], + true + ], + [ + ['package_value' => 28, 'package_value_with_discount' => 25], + true + ], + ]; + } + /** * Returns request data. * @@ -571,47 +629,80 @@ private function setDhlConfig(array $params) private function getRequestData(): array { return [ - 'data' => [ - 'dest_country_id' => 'DE', - 'dest_region_id' => '82', - 'dest_region_code' => 'BER', - 'dest_street' => 'Turmstraße 17', - 'dest_city' => 'Berlin', - 'dest_postcode' => '10559', - 'dest_postal' => '10559', - 'package_value' => '5', - 'package_value_with_discount' => '5', - 'package_weight' => '8.2657', - 'package_qty' => '1', - 'package_physical_value' => '5', - 'free_method_weight' => '5', - 'store_id' => '1', - 'website_id' => '1', - 'free_shipping' => '0', - 'limit_carrier' => null, - 'base_subtotal_incl_tax' => '5', - 'orig_country_id' => 'US', - 'orig_region_id' => '12', - 'orig_city' => 'Fremont', - 'orig_postcode' => '94538', - 'dhl_id' => 'MAGEN_8501', - 'dhl_password' => 'QR2GO1U74X', - 'dhl_account' => '799909537', - 'dhl_shipping_intl_key' => '54233F2B2C4E5C4B4C5E5A59565530554B405641475D5659', - 'girth' => null, - 'height' => null, - 'length' => null, - 'width' => null, - 'weight' => 1, - 'dhl_shipment_type' => 'P', - 'dhl_duitable' => 0, - 'dhl_duty_payment_type' => 'R', - 'dhl_content_desc' => 'Big Box', - 'limit_method' => 'IE', - 'ship_date' => '2014-01-09', - 'action' => 'RateEstimate', - 'all_items' => [], - ] + 'dest_country_id' => 'DE', + 'dest_region_id' => '82', + 'dest_region_code' => 'BER', + 'dest_street' => 'Turmstraße 17', + 'dest_city' => 'Berlin', + 'dest_postcode' => '10559', + 'dest_postal' => '10559', + 'package_value' => '5', + 'package_value_with_discount' => '5', + 'package_weight' => '8.2657', + 'package_qty' => '1', + 'package_physical_value' => '5', + 'free_method_weight' => '5', + 'store_id' => '1', + 'website_id' => '1', + 'free_shipping' => '0', + 'limit_carrier' => null, + 'base_subtotal_incl_tax' => '5', + 'orig_country_id' => 'US', + 'orig_region_id' => '12', + 'orig_city' => 'Fremont', + 'orig_postcode' => '94538', + 'dhl_id' => 'MAGEN_8501', + 'dhl_password' => 'QR2GO1U74X', + 'dhl_account' => '799909537', + 'dhl_shipping_intl_key' => '54233F2B2C4E5C4B4C5E5A59565530554B405641475D5659', + 'girth' => null, + 'height' => null, + 'length' => null, + 'width' => null, + 'weight' => 1, + 'dhl_shipment_type' => 'P', + 'dhl_duitable' => 0, + 'dhl_duty_payment_type' => 'R', + 'dhl_content_desc' => 'Big Box', + 'limit_method' => 'IE', + 'ship_date' => '2014-01-09', + 'action' => 'RateEstimate', + 'all_items' => [], ]; } + + /** + * Set next response content from file + * + * @param string $file + */ + private function setNextResponse(string $file): void + { + //phpcs:disable Magento2.Functions.DiscouragedFunction + $response = new Response( + 200, + [], + file_get_contents($file) + ); + //phpcs:enable Magento2.Functions.DiscouragedFunction + $this->httpClient->nextResponses( + array_fill(0, Carrier::UNAVAILABLE_DATE_LOOK_FORWARD + 1, $response) + ); + } + + /** + * Create Rate Request + * + * @param array $addRequestData + * @return RateRequest + */ + private function createRequest(array $addRequestData = []): RateRequest + { + $requestData = $this->getRequestData(); + if (!empty($addRequestData)) { + $requestData = array_merge($requestData, $addRequestData); + } + + return Bootstrap::getObjectManager()->create(RateRequest::class, ['data' => $requestData]); + } }