diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php b/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php index bfea7dc1295c..d8a933d9f453 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php @@ -72,6 +72,10 @@ public function execute(int $entityId, int $storeId): bool ['child_products' => $this->resourceConnection->getTableName('catalog_product_entity')], 'child_products.entity_id = bundle_selections.product_id', [] + )->joinInner( + ['child_stock_item' => $this->resourceConnection->getTableName('cataloginventory_stock_item')], + 'child_stock_item.product_id = child_products.entity_id', + [] )->group( ['bundle_options.parent_id', 'bundle_options.option_id'] )->where( @@ -103,18 +107,36 @@ public function execute(int $entityId, int $storeId): bool '1', '0' ); + + $hasMinRequiredQuantity = $connection->getCheckSql( + 'required = 1 AND manage_stock = 1 AND selection_can_change_qty = 0', + '(qty >= bundle_selections.selection_qty OR backorders > 0) AND is_in_stock = 1', + '1' + ); + + $requiredInStock = $connection->getCheckSql( + 'required = 1 AND manage_stock = 1 AND selection_can_change_qty = 1', + '(qty >= 0 OR backorders > 0) AND is_in_stock = 1', + '1' + ); + $optionsSaleabilitySelect->columns([ 'required' => 'bundle_options.required', 'is_salable' => $isOptionSalableExpr, 'is_required_and_unsalable' => $isRequiredOptionUnsalable, + 'has_min_required_quantity' => $hasMinRequiredQuantity, + 'required_in_stock' => $requiredInStock ]); $select = $connection->select()->from( $optionsSaleabilitySelect, - [new \Zend_Db_Expr('(MAX(is_salable) = 1 AND MAX(is_required_and_unsalable) = 0)')] + [new \Zend_Db_Expr( + '(MAX(is_salable) = 1 AND MAX(is_required_and_unsalable) = 0)' . + 'AND MIN(required_in_stock) = 1 AND MIN(has_min_required_quantity) = 1' + )] ); - $isSalable = $connection->fetchOne($select); + $isSalable = $connection->fetchOne($select); return (bool) $isSalable; } } diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item/StockItemCriteria.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item/StockItemCriteria.php index 3d82ab247c4a..c65f031abaff 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item/StockItemCriteria.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item/StockItemCriteria.php @@ -10,7 +10,7 @@ use Magento\CatalogInventory\Model\ResourceModel\Stock\Item\StockItemCriteriaMapper; /** - * Class StockItemCriteria + * Build criteria to filter products on catalog_stockinventory table */ class StockItemCriteria extends AbstractCriteria implements \Magento\CatalogInventory\Api\StockItemCriteriaInterface { @@ -55,7 +55,11 @@ public function setScopeFilter($scope) */ public function setProductsFilter($products) { - $this->data['products_filter'] = [$products]; + if (is_array($products)) { + $this->data['products_filter'] = $products; + } else { + $this->data['products_filter'] = [$products]; + } return true; } diff --git a/app/code/Magento/ConfigurableProduct/Model/Inventory/ChangeParentStockStatus.php b/app/code/Magento/ConfigurableProduct/Model/Inventory/ChangeParentStockStatus.php index 4ad15ea905f0..860e507fa6d6 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Inventory/ChangeParentStockStatus.php +++ b/app/code/Magento/ConfigurableProduct/Model/Inventory/ChangeParentStockStatus.php @@ -59,7 +59,7 @@ public function __construct( /** * Update stock status of configurable products based on children products stock status * - * @param array $childrenIds + * @param array $childrenIds * @return void */ public function execute(array $childrenIds): void @@ -70,6 +70,17 @@ public function execute(array $childrenIds): void } } + /** + * Updates the parent stock status based on children statuses + * + * @param int $parentId + * @return void + */ + public function executeFromParent(int $parentId): void + { + $this->processStockForParent($parentId); + } + /** * Update stock status of configurable product based on children products stock status * @@ -106,6 +117,7 @@ private function processStockForParent(int $productId): void if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) { $parentStockItem->setIsInStock($childrenIsInStock); $parentStockItem->setStockStatusChangedAuto(1); + // @phpstan-ignore method.notFound $parentStockItem->setStockStatusChangedAutomaticallyFlag(true); $this->stockItemRepository->save($parentStockItem); } diff --git a/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php index 4ae3efdd6aca..243d57379a92 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php +++ b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php @@ -7,6 +7,7 @@ namespace Magento\ConfigurableProduct\Model\Inventory; +use Magento\Catalog\Model\Product\Type; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\Catalog\Api\Data\ProductInterface as Product; use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; @@ -20,10 +21,7 @@ */ class ParentItemProcessor implements ParentItemProcessorInterface { - /** - * @var ChangeParentStockStatus - */ - private $changeParentStockStatus; + private ChangeParentStockStatus $changeParentStockStatus; /** * @param Configurable $configurableType @@ -50,8 +48,12 @@ public function __construct( * @param Product $product * @return void */ - public function process(Product $product) + public function process(Product $product): void { - $this->changeParentStockStatus->execute([$product->getId()]); + if ($product->getTypeId() === Type::TYPE_SIMPLE) { + $this->changeParentStockStatus->execute([$product->getId()]); + } elseif ($product->getTypeId() === Configurable::TYPE_CODE) { + $this->changeParentStockStatus->executeFromParent((int)$product->getId()); + } } } diff --git a/app/code/Magento/Customer/Helper/Address.php b/app/code/Magento/Customer/Helper/Address.php index 88f3024c2dc7..3e828560c1a1 100644 --- a/app/code/Magento/Customer/Helper/Address.php +++ b/app/code/Magento/Customer/Helper/Address.php @@ -330,7 +330,7 @@ public function convertStreetLines($origStreets, $toCount) } } - return $lines; + return array_filter($lines); } /** diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Multiline.php b/app/code/Magento/Customer/Model/Metadata/Form/Multiline.php index 6dcb4f53d366..8a37b2e2089c 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/Multiline.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/Multiline.php @@ -116,7 +116,7 @@ public function outputValue($format = ElementFactory::OUTPUT_FORMAT_TEXT) $output = implode("
", $values); break; case ElementFactory::OUTPUT_FORMAT_ONELINE: - $output = implode(" ", $values); + $output = trim(implode(" ", $values), ' '); break; default: $output = implode("\n", $values); diff --git a/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php index 2c271f90bfa1..10a6e8d96812 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php @@ -74,7 +74,6 @@ protected function _initSelect() { parent::_initSelect(); $this->joinRegionNameTable(); - return $this; } diff --git a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php index 47021100f7a8..2648b0d83359 100644 --- a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php +++ b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php @@ -242,6 +242,7 @@ public static function getConvertStreetLinesDataProvider() return [ [['street1', 'street2', 'street3', 'street4'], 3, ['street1 street2', 'street3', 'street4']], [['street1', 'street2', 'street3', 'street4'], 2, ['street1 street2', 'street3 street4']], + [['street1', ''], 2, ['street1']] ]; } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Page/FooterTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Page/FooterTest.php index 81c1c5d13f31..1efa68787a04 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/Page/FooterTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Page/FooterTest.php @@ -6,6 +6,10 @@ namespace Magento\Backend\Block\Page; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\App\ProductMetadata; +use Magento\TestFramework\Helper\Bootstrap; + /** * Test \Magento\Backend\Block\Page\Footer * @@ -16,7 +20,7 @@ class FooterTest extends \PHPUnit\Framework\TestCase /** * Test Product Version Value */ - const TEST_PRODUCT_VERSION = '222.333.444'; + private const TEST_PRODUCT_VERSION = '222.333.444'; /** * @var \Magento\Backend\Block\Page\Footer @@ -26,13 +30,14 @@ class FooterTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { parent::setUp(); - $productMetadataMock = $this->getMockBuilder(\Magento\Framework\App\ProductMetadata::class) - ->onlyMethods(['getVersion']) + $productMetadataMock = $this->getMockBuilder(ProductMetadata::class) ->disableOriginalConstructor() ->getMock(); + $productMetadataMock->expects($this->once()) - ->method('getVersion') + ->method('getDistributionVersion') ->willReturn($this::TEST_PRODUCT_VERSION); + $this->block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\View\LayoutInterface::class )->createBlock( @@ -44,6 +49,8 @@ protected function setUp(): void public function testToHtml() { + /** @var \Magento\Framework\App\CacheInterface $cacheManager */ + Bootstrap::getObjectManager()->create(CacheInterface::class); $footerContent = $this->block->toHtml(); $this->assertStringContainsString( 'ver. ' . $this::TEST_PRODUCT_VERSION, diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php index 8031e4e7a584..d62ec931c542 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php @@ -6,6 +6,8 @@ namespace Magento\Bundle\Model\Product; +use Magento\Framework\Exception\NoSuchEntityException; + /** * Test class for \Magento\Bundle\Model\Product\Type (bundle product type) * @@ -216,6 +218,7 @@ public function testIsSaleableOnBundleWithoutSaleableSelectionsOnRequiredOption( /** * Check bundle product is NOT saleable if * there are not enough qty of selection on required option + * when user cannot define own quantities * * @magentoAppIsolation enabled * @covers \Magento\Bundle\Model\Product\Type::isSalable @@ -224,9 +227,7 @@ public function testIsSaleableOnBundleWithoutSaleableSelectionsOnRequiredOption( public function testIsSaleableOnBundleWithNotEnoughQtyOfSelection() { $this->setQtyForSelections(['simple1', 'simple2', 'simple3'], 1); - $bundleProduct = $this->productRepository->get('bundle-product'); - $this->assertFalse( $bundleProduct->isSalable(), 'Bundle product supposed to be non saleable' @@ -354,4 +355,22 @@ private function setQtyForSelections($productsSku, $qty) $this->productRepository->save($product); } } + + /** + * Check bundle product is not salable if required option where user can + * set own quantity is not in stock + * + * @return void + * @magentoAppIsolation enabled + * @throws NoSuchEntityException + */ + public function testIsSalableOnBundleWithRequiredOptionUserCanChangeQtyWithoutStock() + { + $product = $this->productRepository->get('bundle-product-checkbox-required-option'); + $this->setQtyForSelections(['simple1'], 0); + $this->assertFalse( + $product->isSalable(), + 'Bundle product with required option that has 0 stock should not be salable' + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/issaleable_product.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/issaleable_product.php index 960a0ac8c5bb..3877157ee0d7 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/issaleable_product.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/issaleable_product.php @@ -8,6 +8,50 @@ Resolver::getInstance()->requireDataFixture('Magento/Bundle/_files/multiple_products.php'); +if (!function_exists('prepareBundleOptions')) +{ + function prepareBundleOptions(Magento\Catalog\Model\Product $product) { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var \Magento\Catalog\Model\ProductRepository $productRepository */ + $productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + if ($product->getBundleOptionsData()) { + $options = []; + foreach ($product->getBundleOptionsData() as $key => $optionData) { + if (!(bool)$optionData['delete']) { + $option = $objectManager->create(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class) + ->create(['data' => $optionData]); + $option->setSku($product->getSku()); + $option->setOptionId(null); + + $links = []; + $bundleLinks = $product->getBundleSelectionsData(); + if (!empty($bundleLinks[$key])) { + foreach ($bundleLinks[$key] as $linkData) { + if (!(bool)$linkData['delete']) { + $link = $objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class) + ->create(['data' => $linkData]); + $linkProduct = $productRepository->getById($linkData['product_id']); + $link->setSku($linkProduct->getSku()); + $link->setQty($linkData['selection_qty']); + if (isset($linkData['selection_can_change_qty'])) { + $link->setCanChangeQuantity((bool)$linkData['selection_can_change_qty']); + } + $links[] = $link; + } + } + $option->setProductLinks($links); + $options[] = $option; + } + } + } + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); + } + $productRepository->save($product, true); + } +} + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Catalog\Model\ProductRepository $productRepository */ $productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); @@ -177,36 +221,42 @@ ] ); -if ($product->getBundleOptionsData()) { - $options = []; - foreach ($product->getBundleOptionsData() as $key => $optionData) { - if (!(bool)$optionData['delete']) { - $option = $objectManager->create(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class) - ->create(['data' => $optionData]); - $option->setSku($product->getSku()); - $option->setOptionId(null); - - $links = []; - $bundleLinks = $product->getBundleSelectionsData(); - if (!empty($bundleLinks[$key])) { - foreach ($bundleLinks[$key] as $linkData) { - if (!(bool)$linkData['delete']) { - $link = $objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class) - ->create(['data' => $linkData]); - $linkProduct = $productRepository->getById($linkData['product_id']); - $link->setSku($linkProduct->getSku()); - $link->setQty($linkData['selection_qty']); - $links[] = $link; - } - } - $option->setProductLinks($links); - $options[] = $option; - } - } - } - $extension = $product->getExtensionAttributes(); - $extension->setBundleProductOptions($options); - $product->setExtensionAttributes($extension); -} +$bundleProduct = $objectManager->create(\Magento\Catalog\Model\Product::class); +$bundleProduct->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) + ->setId(4) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Bundle Product Checkbox Required Option') + ->setSku('bundle-product-checkbox-required-option') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setPriceView(0) + ->setSkuType(1) + ->setWeightType(1) + ->setPriceType(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC) + ->setPrice(10.0) + ->setShipmentType(Magento\Catalog\Model\Product\Type\AbstractType::SHIPMENT_TOGETHER) + ->setBundleOptionsData([ + [ + 'title' => 'Checkbox Options', + 'default_title' => 'Checkbox Options', + 'type' => 'checkbox', + 'required' => 1, + 'delete' => '', + ], + ]) + ->setBundleSelectionsData([ + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 6 + ], + ] + ]); -$productRepository->save($product, true); +prepareBundleOptions($product); +prepareBundleOptions($bundleProduct); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php index 81a067195e90..d59bff310b51 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php @@ -37,8 +37,7 @@ ->setName('Configurable Product') ->setSku('configurable') ->setVisibility(Visibility::VISIBILITY_BOTH) - ->setStatus(Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); + ->setStatus(Status::STATUS_ENABLED); $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); $product = $productRepository->save($product); @@ -108,3 +107,10 @@ $indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); $indexerProcessor->reindexRow($product->getId()); + +/** @var Item $stockItem */ +$stockItem = Bootstrap::getObjectManager()->create(Item::class); +$stockItem->load($product->getId(), 'product_id'); + +$stockItem->setIsInStock(true); +$stockItem->save(); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Api/AddressRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Api/AddressRepositoryTest.php index 2cb125baaeee..cc54f0a6a0ac 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Api/AddressRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Api/AddressRepositoryTest.php @@ -65,7 +65,7 @@ protected function setUp(): void $address2 = $this->_addressFactory->create() ->setId('2') ->setCountryId('US') - ->setCustomerId(1) + ->setCustomerId('1') ->setPostcode('47676') ->setRegion($region) ->setStreet(['Black str, 48']) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/EditTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/EditTest.php index e62eb5a8ac3a..3b50155a15f0 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/EditTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/EditTest.php @@ -8,6 +8,7 @@ namespace Magento\Customer\Block\Form; use Magento\Customer\Model\Session; +use Magento\Framework\View\Element\ButtonLockInterface; use Magento\Framework\View\LayoutInterface; use Magento\Framework\View\Element\ButtonLockManager; use Magento\TestFramework\Helper\Bootstrap; @@ -60,7 +61,7 @@ protected function setUp(): void public function testCustomerEditButton(): void { $code = 'customer_edit'; - $buttonLock = $this->getMockBuilder(\Magento\ReCaptchaUi\Model\ButtonLock::class) + $buttonLock = $this->getMockBuilder(ButtonLockInterface::class) ->disableOriginalConstructor() ->disableAutoload() ->onlyMethods(['isDisabled', 'getCode']) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php index 2d15819f0933..25c65ac5eec3 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php @@ -8,6 +8,7 @@ namespace Magento\Customer\Block\Form; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Element\ButtonLockInterface; use Magento\Framework\View\LayoutInterface; use Magento\Framework\View\Element\ButtonLockManager; use Magento\TestFramework\Helper\Bootstrap; @@ -49,7 +50,7 @@ protected function setUp(): void $this->layout = $this->objectManager->get(LayoutInterface::class); $code = 'customer_login_form_submit'; - $buttonLock = $this->getMockBuilder(\Magento\ReCaptchaUi\Model\ButtonLock::class) + $buttonLock = $this->getMockBuilder(ButtonLockInterface::class) ->disableOriginalConstructor() ->disableAutoload() ->onlyMethods(['isDisabled', 'getCode']) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php index d1da64e0e9fe..24963e55d53f 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php @@ -7,6 +7,7 @@ use Magento\Customer\Block\DataProviders\AddressAttributeData; use Magento\Customer\ViewModel\Address\RegionProvider; +use Magento\Framework\View\Element\ButtonLockInterface; use Magento\Framework\View\Element\Template; use Magento\Framework\View\Element\ButtonLockManager; use Magento\TestFramework\Helper\Bootstrap; @@ -207,7 +208,7 @@ private function setRegionProvider(Template $block): void private function setButtonLockManager(Template $block): void { $code = 'customer_create_form_submit'; - $buttonLock = $this->getMockBuilder(\Magento\ReCaptchaUi\Model\ButtonLock::class) + $buttonLock = $this->getMockBuilder(ButtonLockInterface::class) ->disableOriginalConstructor() ->disableAutoload() ->onlyMethods(['isDisabled', 'getCode']) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php index 87983dd0277f..2557931918ad 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php @@ -81,15 +81,15 @@ protected function setUp(): void $region = $this->regionFactory->create() ->setRegionCode('AL') ->setRegion('Alabama') - ->setRegionId(1); + ->setRegionId('1'); $address = $this->addressFactory->create() ->setId('1') ->setCountryId('US') ->setCustomerId('1') ->setPostcode('75477') ->setRegion($region) - ->setRegionId(1) - ->setStreet(['Green str, 67']) + ->setRegionId('1') + ->setStreet(['Green str, 67', '']) ->setTelephone('3468676') ->setCity('CityM') ->setFirstname('John') diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/Grid/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/Grid/CollectionTest.php index 07e38b1fd52f..c9ec2d35aef9 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/Grid/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/Grid/CollectionTest.php @@ -55,10 +55,8 @@ public function testGetItemByIdForUpdateOnSchedule() $this->assertNotEmpty($item); $this->assertSame($newCustomer->getEmail(), $item->getEmail()); $this->assertSame('test street test city Armed Forces Middle East 01001', $item->getBillingFull()); - /** set customer grid indexer on schedule' mode */ $indexer->setScheduled(true); - /** Verify after update */ $newCustomer->setEmail('customer_updated@example.com'); $this->customerRepository->save($newCustomer); diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Export/AddressTest.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Export/AddressTest.php index 20a757508c42..c215fd2aef0e 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Export/AddressTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Export/AddressTest.php @@ -7,6 +7,8 @@ namespace Magento\CustomerImportExport\Model\Export; use Magento\CustomerImportExport\Model\Import\Address as ImportAddress; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\TestFramework\ObjectManager; /** * Test for customer address export model @@ -23,7 +25,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase /** * List of existing websites * - * @var array + * @var WebsiteInterface[] */ protected $_websites = []; @@ -39,7 +41,7 @@ protected function setUp(): void )->getWebsites( true ); - /** @var $website \Magento\Store\Model\Website */ + /** @var WebsiteInterface $website */ foreach ($websites as $website) { $this->_websites[$website->getId()] = $website->getCode(); } @@ -48,18 +50,18 @@ protected function setUp(): void /** * Test export method */ - public function testExport() + public function testExport(): void { $websiteCode = Address::COLUMN_WEBSITE; $emailCode = Address::COLUMN_EMAIL; $entityIdCode = Address::COLUMN_ADDRESS_ID; $expectedAttributes = []; - /** @var $collection \Magento\Customer\Model\ResourceModel\Address\Attribute\Collection */ + /** @var \Magento\Customer\Model\ResourceModel\Address\Attribute\Collection $collection */ $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Customer\Model\ResourceModel\Address\Attribute\Collection::class ); - /** @var $attribute \Magento\Customer\Model\Attribute */ + /** @var \Magento\Customer\Model\Attribute $attribute */ foreach ($collection as $attribute) { $expectedAttributes[] = $attribute->getAttributeCode(); } @@ -73,7 +75,6 @@ public function testExport() ) ); $this->_model->setParameters([]); - $data = $this->_csvToArray($this->_model->export(), $entityIdCode); $this->assertEquals( @@ -84,18 +85,18 @@ public function testExport() $this->assertNotEmpty($data['data'], 'No data was exported'); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var ObjectManager $objectManager */ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); // Get addresses - /** @var $customers \Magento\Customer\Model\Customer[] */ + /** @var \Magento\Customer\Model\Customer[] $customers */ $customers = $objectManager->get( \Magento\Framework\Registry::class )->registry( '_fixture/Magento_ImportExport_Customers_Array' ); foreach ($customers as $customer) { - /** @var $address \Magento\Customer\Model\Address */ + /** @var \Magento\Customer\Model\Address $address */ foreach ($customer->getAddresses() as $address) { // Check unique key $data['data'][$address->getId()][$websiteCode] = $this->_websites[$customer->getWebsiteId()]; @@ -128,9 +129,9 @@ public function testExport() /** * Get possible gender values for filter * - * @return array + * @return array> */ - public static function getGenderFilterValueDataProvider() + public static function getGenderFilterValueDataProvider(): array { return ['male' => ['genderFilterValue' => 1], 'female' => ['genderFilterValue' => 2]]; } @@ -142,7 +143,7 @@ public static function getGenderFilterValueDataProvider() * * @param int $genderFilterValue */ - public function testExportWithFilter($genderFilterValue) + public function testExportWithFilter($genderFilterValue): void { $entityIdCode = Address::COLUMN_ADDRESS_ID; @@ -156,11 +157,11 @@ public function testExportWithFilter($genderFilterValue) $this->_model->setParameters($filterData); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var ObjectManager $objectManager */ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); // Get expected address count - /** @var $customers \Magento\Customer\Model\Customer[] */ + /** @var \Magento\Customer\Model\Customer[] $customers */ $customers = $objectManager->get( \Magento\Framework\Registry::class )->registry( @@ -168,6 +169,7 @@ public function testExportWithFilter($genderFilterValue) ); $expectedCount = 0; foreach ($customers as $customer) { + // @phpstan-ignore method.notFound if ($customer->getGender() == $genderFilterValue) { $expectedCount += count($customer->getAddresses()); } @@ -181,7 +183,7 @@ public function testExportWithFilter($genderFilterValue) /** * Test entity type code value */ - public function testGetEntityTypeCode() + public function testGetEntityTypeCode(): void { $this->assertEquals('customer_address', $this->_model->getEntityTypeCode()); } @@ -189,7 +191,7 @@ public function testGetEntityTypeCode() /** * Test type of attribute collection */ - public function testGetAttributeCollection() + public function testGetAttributeCollection(): void { $this->assertInstanceOf( \Magento\Customer\Model\ResourceModel\Address\Attribute\Collection::class, @@ -202,13 +204,16 @@ public function testGetAttributeCollection() * * @param string $content * @param mixed $entityId - * @return array + * @return array> */ - protected function _csvToArray($content, $entityId = null) + protected function _csvToArray($content, $entityId = null): array { $data = ['header' => [], 'data' => []]; - - $lines = str_getcsv($content, "\n", '"', '\\'); + // Cannot use str_getcsv here because if native php bug. Needs to split lines + // the homemade way. + // Though this test is very fragile for now. This homemade way of splitting the CSV + // can break easily if something changes in the adress's street. + $lines = array_filter(preg_split('/\n(?!")/m', $content)); foreach ($lines as $index => $line) { if ($index == 0) { $data['header'] = str_getcsv($line, ',', '"', '\\'); @@ -228,7 +233,7 @@ protected function _csvToArray($content, $entityId = null) /** * Test filename getter. Filename must be set in constructor. */ - public function testGetFileName() + public function testGetFileName(): void { $this->assertEquals($this->_model->getEntityTypeCode(), $this->_model->getFileName()); } diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/Block/Sales/Order/Email/Items/Order/DownloadableTest.php b/dev/tests/integration/testsuite/Magento/Downloadable/Block/Sales/Order/Email/Items/Order/DownloadableTest.php index 107d492068e4..a11c93d245b5 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/Block/Sales/Order/Email/Items/Order/DownloadableTest.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/Block/Sales/Order/Email/Items/Order/DownloadableTest.php @@ -52,15 +52,15 @@ protected function setUp(): void } /** - * @magentoDataFixture Magento/Downloadable/_files/quote_with_configurable_downloadable_product.php + * @magentoDataFixture Magento/Downloadable/_files/quote_with_downloadable_product.php * @return void */ public function testShouldSendDownloadableLinksInTheEmail(): void { /** @var Quote $quote */ $quote = $this->objectManager->create(Quote::class); - $quote->load('reserved_order_configurable_downloadable', 'reserved_order_id'); - + // @phpstan-ignore argument.type + $quote->load('reserved_order_id_1', 'reserved_order_id'); $checkoutSession = $this->objectManager->get(CheckoutSession::class); $checkoutSession->setQuoteId($quote->getId()); @@ -74,7 +74,7 @@ public function testShouldSendDownloadableLinksInTheEmail(): void $cartManagement->placeOrder($cartId); $message = $this->transportBuilder->getSentMessage(); $rawMessage = quoted_printable_decode($message->getBody()->bodyToString()); - $this->assertStringContainsString('Configurable Downloadable Product', $rawMessage); + $this->assertStringContainsString('Downloadable Product', $rawMessage); $this->assertStringContainsString('SKU: downloadable-product', $rawMessage); $this->assertStringContainsString('Downloadable Product Link', $rawMessage); $this->assertStringContainsString('/downloadable/download/link/id/', $rawMessage); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/quote_with_configurable_downloadable_product.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/quote_with_configurable_downloadable_product.php deleted file mode 100644 index bb17a119f0d6..000000000000 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/quote_with_configurable_downloadable_product.php +++ /dev/null @@ -1,87 +0,0 @@ -requireDataFixture('Magento/Downloadable/_files/product_configurable_downloadable.php'); - -$objectManager = Bootstrap::getObjectManager(); -/** @var ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->create(ProductRepositoryInterface::class); -$downloadableProduct = $productRepository->get('downloadable-product'); -$configurableProduct = $productRepository->get('configurable_downloadable'); -/** @var $options Collection */ -$options = $objectManager->create(Collection::class); -/** @var Config $eavConfig */ -$eavConfig = $objectManager->get(Config::class); -$attribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable'); -$option = $options->setAttributeFilter($attribute->getId())->getFirstItem(); -$requestInfo = new DataObject( - [ - 'product_id' => $configurableProduct->getId(), - 'selected_configurable_option' => $downloadableProduct->getId(), - 'qty' => 1, - 'super_attribute' => [ - $attribute->getId() => $option->getId() - ], - 'links' => array_keys($downloadableProduct->getDownloadableLinks()) - ] -); -$addressData = [ - 'telephone' => 3234676, - 'postcode' => 47676, - 'country_id' => 'DE', - 'city' => 'CityX', - 'street' => ['Black str, 48'], - 'lastname' => 'Smith', - 'firstname' => 'John', - 'vat_id' => 12345, - 'address_type' => 'shipping', - 'email' => 'some_email@mail.com', -]; - -$billingAddress = $objectManager->create( - Address::class, - ['data' => $addressData] -); -$billingAddress->setAddressType('billing'); -$shippingAddress = clone $billingAddress; -$shippingAddress->setId(null)->setAddressType('shipping'); - -/** @var Quote $quote */ -$quote = $objectManager->create(Quote::class); -$quote->setCustomerIsGuest(true) - ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) - ->setReservedOrderId('reserved_order_configurable_downloadable') - ->setIsMultiShipping(false) - ->setBillingAddress($billingAddress) - ->setShippingAddress($shippingAddress) - ->addProduct($configurableProduct, $requestInfo); - -$quote->getPayment()->setMethod('checkmo'); -$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate')->setCollectShippingRates(true); -$quote->collectTotals(); -$quote->save(); - -/** @var QuoteIdMask $quoteIdMask */ -$quoteIdMask = $objectManager - ->create(QuoteIdMaskFactory::class) - ->create(); -$quoteIdMask->setQuoteId($quote->getId()); -$quoteIdMask->setDataChanges(true); -$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/quote_with_configurable_downloadable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/quote_with_configurable_downloadable_product_rollback.php deleted file mode 100644 index 46fecf75bb58..000000000000 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/quote_with_configurable_downloadable_product_rollback.php +++ /dev/null @@ -1,21 +0,0 @@ -create(\Magento\Quote\Model\Quote::class); -$quote->load('reserved_order_configurable_downloadable', 'reserved_order_id')->delete(); - -/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ -$quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class); -$quoteIdMask->delete($quote->getId()); - -Resolver::getInstance()->requireDataFixture( - 'Magento/Downloadable/_files/product_configurable_downloadable_rollback.php' -); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/quote_with_downloadable_product.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/quote_with_downloadable_product.php index ecbba524ba82..dd6f2bca76f1 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/quote_with_downloadable_product.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/quote_with_downloadable_product.php @@ -6,6 +6,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Quote\Model\Quote\Address; Resolver::getInstance()->requireDataFixture('Magento/Downloadable/_files/product_downloadable.php'); @@ -15,6 +16,27 @@ $productRepository = $objectManager->create(ProductRepositoryInterface::class); $product = $productRepository->get('downloadable-product'); +$addressData = [ + 'telephone' => 3234676, + 'postcode' => 47676, + 'country_id' => 'DE', + 'city' => 'CityX', + 'street' => ['Black str, 48'], + 'lastname' => 'Smith', + 'firstname' => 'John', + 'vat_id' => 12345, + 'address_type' => 'shipping', + 'email' => 'some_email@mail.com', +]; + +$billingAddress = $objectManager->create( + Address::class, + ['data' => $addressData] +); +$billingAddress->setAddressType('billing'); +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + /** @var \Magento\Quote\Model\Quote $quote */ $quote = $objectManager->create(\Magento\Quote\Model\Quote::class); $quote->setCustomerIsGuest( @@ -26,13 +48,19 @@ )->setReservedOrderId( 'reserved_order_id_1' )->setIsMultiShipping( - false -)->addProduct( + 0 +)->setBillingAddress($billingAddress) +->setShippingAddress($shippingAddress) +->addProduct( + // @phpstan-ignore argument.type $product, new \Magento\Framework\DataObject([ + // @phpstan-ignore method.notFound 'links' => array_keys($product->getDownloadableLinks()) ]) ); +$quote->getPayment()->setMethod('checkmo'); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate')->setCollectShippingRates(true); $quote->collectTotals(); $quote->save(); @@ -40,6 +68,7 @@ $quoteIdMask = $objectManager ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) ->create(); +// @phpstan-ignore method.notFound $quoteIdMask->setQuoteId($quote->getId()); $quoteIdMask->setDataChanges(true); $quoteIdMask->save();