From 9d99362a318233eec046f16368154a4e57462923 Mon Sep 17 00:00:00 2001 From: Rafal Janicki Date: Tue, 30 Apr 2024 11:21:09 +0100 Subject: [PATCH 1/4] LYNX-339: private_content_version cookie returned in GQL queries --- .../GraphQl/PageCache/DisableSessionTest.php | 72 ++++++++++++++++ .../Framework/App/PageCache/Version.php | 84 +++++++++++-------- 2 files changed, 119 insertions(+), 37 deletions(-) create mode 100644 dev/tests/Magento/GraphQl/PageCache/DisableSessionTest.php diff --git a/dev/tests/Magento/GraphQl/PageCache/DisableSessionTest.php b/dev/tests/Magento/GraphQl/PageCache/DisableSessionTest.php new file mode 100644 index 000000000000..5614eccff88b --- /dev/null +++ b/dev/tests/Magento/GraphQl/PageCache/DisableSessionTest.php @@ -0,0 +1,72 @@ +graphQlMutationWithResponseHeaders($this->getMutation()); + $this->assertArrayHasKey('headers', $result); + if (!empty($result['headers']['Set-Cookie'])) { + $this->assertStringNotContainsString( + Version::COOKIE_NAME, + $result['headers']['Set-Cookie'], + Version::COOKIE_NAME . ' should not be present in Set-Cookie header' + ); + } + } + + #[ + Config('graphql/session/disable', '0') + ] + public function testPrivateSessionContentCookiePresentWhenSessionEnabled() + { + $result = $this->graphQlMutationWithResponseHeaders($this->getMutation()); + $this->assertArrayHasKey('headers', $result); + $this->assertArrayHasKey('Set-Cookie', $result['headers'], 'Set-Cookie HTTP response header should be present'); + $this->assertStringContainsString( + Version::COOKIE_NAME, + $result['headers']['Set-Cookie'], + Version::COOKIE_NAME . ' should be set by the server' + ); + } + + /** + * Provides dummy mutation to test GraphQl HTTP POST response + * + * @return string + */ + private function getMutation(): string + { + return <<cookieManager = $cookieManager; - $this->request = $request; - $this->cookieMetadataFactory = $cookieMetadataFactory; } /** @@ -61,7 +52,7 @@ public function __construct( * * @return string */ - protected function generateValue() + protected function generateValue(): string { //phpcs:ignore return md5(rand() . time()); @@ -75,16 +66,35 @@ protected function generateValue() * * @return void */ - public function process() + public function process(): void { - if ($this->request->isPost()) { - $publicCookieMetadata = $this->cookieMetadataFactory->createPublicCookieMetadata() - ->setDuration(self::COOKIE_PERIOD) - ->setPath('/') - ->setSecure($this->request->isSecure()) - ->setHttpOnly(false) - ->setSameSite('Lax'); - $this->cookieManager->setPublicCookie(self::COOKIE_NAME, $this->generateValue(), $publicCookieMetadata); + if (!$this->request->isPost()) { + return; } + + if ($this->request->getOriginalPathInfo() === '/graphql' && $this->isSessionDisabled() === true) { + return; + } + + $publicCookieMetadata = $this->cookieMetadataFactory->createPublicCookieMetadata() + ->setDuration(self::COOKIE_PERIOD) + ->setPath('/') + ->setSecure($this->request->isSecure()) + ->setHttpOnly(false) + ->setSameSite('Lax'); + $this->cookieManager->setPublicCookie(self::COOKIE_NAME, $this->generateValue(), $publicCookieMetadata); + } + + /** + * Returns configuration setting for disable session for GraphQl + * + * @return bool + */ + private function isSessionDisabled(): bool + { + return (bool)$this->scopeConfig->getValue( + self::XML_PATH_GRAPHQL_DISABLE_SESSION, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); } } From beb7f44929d95f404b25ffe065a2d85a9d9514a2 Mon Sep 17 00:00:00 2001 From: Rafal Janicki Date: Wed, 8 May 2024 11:18:58 +0100 Subject: [PATCH 2/4] LYNX-399: Placeholder thumbnail returns when a simple product added to cart within a grouped product --- .../Model/Product/Type/Grouped.php | 57 +++-- .../AddGroupedProductToCartThumbnailTest.php | 224 ++++++++++++++++++ 2 files changed, 261 insertions(+), 20 deletions(-) create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddGroupedProductToCartThumbnailTest.php diff --git a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php index 24e4e49f51b9..8365b3ac1161 100644 --- a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php @@ -1,13 +1,13 @@ getAssociatedProductCollection( $product )->addAttributeToSelect( - ['name', 'price', 'special_price', 'special_from_date', 'special_to_date', 'tax_class_id', 'image'] + [ + 'name', + 'price', + 'special_price', + 'special_from_date', + 'special_to_date', + 'tax_class_id', + 'image', + 'thumbnail' + ] )->addFilterByRequiredOptions()->setPositionOrder()->addStoreFilter( $this->getStoreFilter($product) )->addAttributeToFilter( @@ -347,22 +352,34 @@ protected function getProductInfo(\Magento\Framework\DataObject $buyRequest, $pr return __('Please specify the quantity of product(s).')->render(); } foreach ($associatedProducts as $subProduct) { - if (!isset($productsInfo[$subProduct->getId()])) { - if ($isStrictProcessMode && !$subProduct->getQty() && $subProduct->isSalable()) { - return __('Please specify the quantity of product(s).')->render(); - } - if (isset($buyRequest['qty']) && !isset($buyRequest['super_group'])) { - $subProductQty = (float)$subProduct->getQty() * (float)$buyRequest['qty']; - $productsInfo[$subProduct->getId()] = $subProduct->isSalable() ? $subProductQty : 0; - } else { - $productsInfo[$subProduct->getId()] = $subProduct->isSalable() ? (float)$subProduct->getQty() : 0; - } + if (isset($productsInfo[$subProduct->getId()])) { + continue; + } + if ($isStrictProcessMode && !$subProduct->getQty() && $subProduct->isSalable()) { + return __('Please specify the quantity of product(s).')->render(); } + $productsInfo[$subProduct->getId()] = $this->getSubProductQtyInfo($buyRequest, $subProduct); } - return $productsInfo; } + /** + * Gets qty info for sub product in group + * + * @param DataObject $buyRequest + * @param Product $subProduct + * @return float + */ + private function getSubProductQtyInfo( + DataObject $buyRequest, + Product $subProduct, + ): float { + if (isset($buyRequest['qty']) && !isset($buyRequest['super_group'])) { + return $subProduct->isSalable() ? $subProduct->getQty() * (float)$buyRequest['qty'] : 0.0; + } + return $subProduct->isSalable() ? $subProduct->getQty() : 0.0; + } + /** * Prepare product and its configuration to be added to some products list. * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddGroupedProductToCartThumbnailTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddGroupedProductToCartThumbnailTest.php new file mode 100644 index 000000000000..fd7a09a77410 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddGroupedProductToCartThumbnailTest.php @@ -0,0 +1,224 @@ + 'Category'], 'category'), + DataFixture( + ProductFixture::class, + [ + 'name' => 'Product 1', + 'sku' => 'product-1', + 'category_ids' => ['$category.id$'], + 'price' => 10 + ], + 'product1' + ), + DataFixture( + ProductFixture::class, + [ + 'name' => 'Product 2', + 'sku' => 'product-2', + 'category_ids' => ['$category.id$'], + 'price' => 15 + ], + 'product2' + ), + DataFixture( + GroupedProductFixture::class, + [ + 'sku' => 'grouped-product', + 'category_ids' => ['$category.id$'], + 'product_links' => [ + ['sku' => '$product1.sku$', 'qty' => 1], + ['sku' => '$product2.sku$', 'qty' => 1] + ] + ], + 'grouped-product' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + ] + public function testAddGroupedProductToCartWithoutImageShouldUseThumbnail() + { + $cartId = DataFixtureStorageManager::getStorage()->get('quoteIdMask')->getMaskedId(); + $groupedProductId = DataFixtureStorageManager::getStorage()->get('grouped-product')->getSku(); + $response = $this->graphQlMutation($this->getMutation($cartId, $groupedProductId)); + + $this->assertArrayHasKey('addProductsToCart', $response); + $this->assertEquals(2, count($response['addProductsToCart']['cart']['itemsV2']['items'])); + $this->assertStringContainsString( + self::DEFAULT_THUMBNAIL_PATH, + $response['addProductsToCart']['cart']['itemsV2']['items'][0]['product']['thumbnail']['url'] + ); + $this->assertStringContainsString( + self::DEFAULT_THUMBNAIL_PATH, + $response['addProductsToCart']['cart']['itemsV2']['items'][1]['product']['thumbnail']['url'] + ); + } + + #[ + ConfigFixture('checkout/cart/grouped_product_image', 'itself'), + DataFixture(CategoryFixture::class, ['name' => 'Category'], 'category'), + DataFixture( + ProductFixture::class, + [ + 'name' => 'Product 1', + 'sku' => 'product-1', + 'category_ids' => ['$category.id$'], + 'price' => 10, + 'media_gallery_entries' => [ + [ + 'label' => 'image', + 'media_type' => 'image', + 'position' => 1, + 'disabled' => false, + 'types' => [ + 'image', + 'small_image', + 'thumbnail' + ], + 'file' => '/m/product1.jpg', + ], + ], + ], + 'product1' + ), + DataFixture( + ProductFixture::class, + [ + 'name' => 'Product 2', + 'sku' => 'product-2', + 'category_ids' => ['$category.id$'], + 'price' => 15, + 'media_gallery_entries' => [ + [ + 'label' => 'image', + 'media_type' => 'image', + 'position' => 1, + 'disabled' => false, + 'types' => [ + 'image', + 'small_image', + 'thumbnail' + ], + 'file' => '/m/product2.jpg', + ], + ], + ], + 'product2' + ), + DataFixture( + GroupedProductFixture::class, + [ + 'sku' => 'grouped-product', + 'category_ids' => ['$category.id$'], + 'product_links' => [ + ['sku' => '$product1.sku$', 'qty' => 1], + ['sku' => '$product2.sku$', 'qty' => 1] + ] + ], + 'grouped-product' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + ] + public function testAddGroupedProductToCartWithImageShouldUseProductImageAsThumbnail() + { + $cartId = DataFixtureStorageManager::getStorage()->get('quoteIdMask')->getMaskedId(); + $groupedProductId = DataFixtureStorageManager::getStorage()->get('grouped-product')->getSku(); + $product1 = DataFixtureStorageManager::getStorage()->get('product1'); + $product2 = DataFixtureStorageManager::getStorage()->get('product2'); + + $response = $this->graphQlMutation($this->getMutation($cartId, $groupedProductId)); + + $this->assertArrayHasKey('addProductsToCart', $response); + $this->assertEquals(2, count($response['addProductsToCart']['cart']['itemsV2']['items'])); + $this->assertStringContainsString( + $product1->getCustomAttribute('thumbnail')->getValue(), + $response['addProductsToCart']['cart']['itemsV2']['items'][0]['product']['thumbnail']['url'] + ); + $this->assertStringContainsString( + $product2->getCustomAttribute('thumbnail')->getValue(), + $response['addProductsToCart']['cart']['itemsV2']['items'][1]['product']['thumbnail']['url'] + ); + } + + /** + * Get addProductsToCart mutation based on passed parameters + * + * @param string $cartId + * @param string $sku + * @return string + */ + private function getMutation( + string $cartId, + string $sku + ): string { + return << Date: Wed, 8 May 2024 19:59:39 +0530 Subject: [PATCH 3/4] LYNX-402: Internal server error when trying to get priceDetails for Bundle products with dynamic price * LYNX-402: Fix DivisionByZeroError at Magento/BundleGraphQl/Model/Resolver/BundlePriceDetails.php:31 * LYNX-402: Test coverage for division by zero error * LYNX-402: Internal server error when trying to get priceDetails for Bundle products with dynamic price Moved the test coverage to dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMainPriceTest.php from dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php --------- Co-authored-by: Sergio Vera --- .../Model/Resolver/BundlePriceDetails.php | 2 +- .../Bundle/BundleProductMainPriceTest.php | 95 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundlePriceDetails.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundlePriceDetails.php index 004b98164641..056138bd0b0a 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/BundlePriceDetails.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundlePriceDetails.php @@ -28,7 +28,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $price = $product->getPrice(); $finalPrice = $product->getFinalPrice(); - $discountPercentage = 100 - (($finalPrice * 100) / $price); + $discountPercentage = ($price) ? (100 - (($finalPrice * 100) / $price)) : 0; return [ 'main_price' => $price, 'main_final_price' => $finalPrice, diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMainPriceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMainPriceTest.php index d745625c1840..7bc57d81be21 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMainPriceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMainPriceTest.php @@ -7,10 +7,38 @@ namespace Magento\GraphQl\Bundle; +use Magento\Bundle\Test\Fixture\AddProductToCart as AddBundleProductToCart; +use Magento\Bundle\Test\Fixture\Link as BundleSelectionFixture; +use Magento\Bundle\Test\Fixture\Option as BundleOptionFixture; +use Magento\Bundle\Test\Fixture\Product as BundleProductFixture; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; class BundleProductMainPriceTest extends GraphQlAbstract { + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @var DataFixtureStorage + */ + private $fixtures; + + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->fixtures = DataFixtureStorageManager::getStorage(); + } + public function getQuery() { $productSku = 'fixed_bundle_product_with_special_price'; @@ -129,4 +157,71 @@ public function testBundleProductPriceDetails(): void $this->assertEquals(40.0, $priceDetails['main_final_price']); $this->assertEquals(20, $priceDetails['discount_percentage']); } + + #[ + DataFixture(ProductFixture::class, ['sku' => 'simple1', 'price' => 10], as:'p1'), + DataFixture(ProductFixture::class, ['sku' => 'simple2', 'price' => 20], as:'p2'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$p1.sku$'], as:'link1'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$p2.sku$'], as:'link2'), + DataFixture(BundleOptionFixture::class, ['title' => 'Checkbox Options', 'type' => 'checkbox', + 'required' => 1,'product_links' => ['$link1$', '$link2$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['title' => 'Checkbox Options', 'type' => 'checkbox', + 'required' => 1,'product_links' => ['$link1$', '$link2$']], 'opt2'), + DataFixture( + BundleProductFixture::class, + ['sku' => 'bundle-product-multiselect-checkbox-options', '_options' => ['$opt1$', '$opt2$']], + as:'bp1' + ), + DataFixture(GuestCartFixture::class, as: 'guestCart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$guestCart.id$', + 'product_id' => '$bp1.id$', + 'selections' => [['$p1.id$'], ['$p2.id$']], + 'qty' => 2 + ] + ) + ] + public function testCartBundleProductPriceDetails() + { + $guestCart = $this->fixtures->get('guestCart'); + $guestQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$guestCart->getId()); + + $cartQuery = $this->getGuestCartQuery($guestQuoteMaskedId); + $cartResponse = $this->graphQlMutation($cartQuery); + $productPriceDetails = $cartResponse['cart']['itemsV2']['items'][0]['product']['price_details']; + self::assertArrayHasKey('main_price', $productPriceDetails); + self::assertArrayHasKey('main_final_price', $productPriceDetails); + self::assertArrayHasKey('discount_percentage', $productPriceDetails); + self::assertEquals(0, $productPriceDetails['main_price']); + self::assertEquals(30, $productPriceDetails['main_final_price']); + self::assertEquals(0, $productPriceDetails['discount_percentage']); + } + + private function getGuestCartQuery(string $maskedId): string + { + return << Date: Thu, 9 May 2024 15:38:34 +0200 Subject: [PATCH 4/4] LYNX_387: Fix bundle original_row_total field (#238) --- .../Bundle/Model/Product/OriginalPrice.php | 133 ++++ ...UpdateBundleQuoteItemBaseOriginalPrice.php | 69 ++ app/code/Magento/Bundle/etc/di.xml | 5 + .../Bundle/BundleProductCartPricesTest.php | 704 ++++++++++++++++++ 4 files changed, 911 insertions(+) create mode 100644 app/code/Magento/Bundle/Model/Product/OriginalPrice.php create mode 100644 app/code/Magento/Bundle/Plugin/Quote/UpdateBundleQuoteItemBaseOriginalPrice.php create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductCartPricesTest.php diff --git a/app/code/Magento/Bundle/Model/Product/OriginalPrice.php b/app/code/Magento/Bundle/Model/Product/OriginalPrice.php new file mode 100644 index 000000000000..ca3a616f3ee0 --- /dev/null +++ b/app/code/Magento/Bundle/Model/Product/OriginalPrice.php @@ -0,0 +1,133 @@ +hasCustomOptions()) { + return $price; + } + + $selectionIds = $this->getBundleSelectionIds($product); + + if (empty($selectionIds)) { + return $price; + } + + $selections = $product->getTypeInstance()->getSelectionsByIds($selectionIds, $product); + foreach ($selections->getItems() as $selection) { + if (!$selection->isSalable()) { + continue; + } + + $selectionQty = $product->getCustomOption('selection_qty_' . $selection->getSelectionId()); + if ($selectionQty) { + $price += $this->getSelectionOriginalTotalPrice( + $product, + $selection, + (float) $selectionQty->getValue() + ); + } + } + + return $price; + } + + /** + * Calculate total original price of selection + * + * @param Product $bundleProduct + * @param Product $selectionProduct + * @param float $selectionQty + * + * @return float + */ + private function getSelectionOriginalTotalPrice( + Product $bundleProduct, + Product $selectionProduct, + float $selectionQty + ): float { + $price = $this->getSelectionOriginalPrice($bundleProduct, $selectionProduct); + + return $price * $selectionQty; + } + + /** + * Calculate the original price of selection + * + * @param Product $bundleProduct + * @param Product $selectionProduct + * + * @return float + */ + public function getSelectionOriginalPrice(Product $bundleProduct, Product $selectionProduct): float + { + if ($bundleProduct->getPriceType() == Price::PRICE_TYPE_DYNAMIC) { + return (float) $selectionProduct->getPrice(); + } + if ($selectionProduct->getSelectionPriceType()) { + // percent + return $bundleProduct->getPrice() * ($selectionProduct->getSelectionPriceValue() / 100); + } + + // fixed + return (float) $selectionProduct->getSelectionPriceValue(); + } + + /** + * Retrieve array of bundle selection IDs + * + * @param Product $product + * @return array + */ + private function getBundleSelectionIds(Product $product): array + { + $customOption = $product->getCustomOption('bundle_selection_ids'); + if ($customOption) { + $selectionIds = $this->serializer->unserialize($customOption->getValue()); + if (is_array($selectionIds)) { + return $selectionIds; + } + } + return []; + } +} diff --git a/app/code/Magento/Bundle/Plugin/Quote/UpdateBundleQuoteItemBaseOriginalPrice.php b/app/code/Magento/Bundle/Plugin/Quote/UpdateBundleQuoteItemBaseOriginalPrice.php new file mode 100644 index 000000000000..89b729d2a645 --- /dev/null +++ b/app/code/Magento/Bundle/Plugin/Quote/UpdateBundleQuoteItemBaseOriginalPrice.php @@ -0,0 +1,69 @@ +getAllVisibleItems() as $quoteItem) { + if ($quoteItem->getProductType() === Type::TYPE_CODE) { + $price = $quoteItem->getProduct()->getPrice(); + $price += $this->price->getTotalBundleItemsOriginalPrice($quoteItem->getProduct()); + $quoteItem->setBaseOriginalPrice($price); + } + } + return $result; + } +} diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml index 3ddefc1a0559..210b0e091b89 100644 --- a/app/code/Magento/Bundle/etc/di.xml +++ b/app/code/Magento/Bundle/etc/di.xml @@ -284,4 +284,9 @@ + + + diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductCartPricesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductCartPricesTest.php new file mode 100644 index 000000000000..a00031f4b633 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductCartPricesTest.php @@ -0,0 +1,704 @@ +quoteIdToMaskedQuoteIdInterface = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->fixtures = $objectManager->get(DataFixtureStorageManager::class)->getStorage(); + } + + #[ + DataFixture(ProductFixture::class, ['price' => 20], 'product1'), + DataFixture(ProductFixture::class, ['price' => 10], 'product2'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$product1.sku$'], 'selection1'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$product2.sku$'], 'selection2'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection1$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection2$']], 'opt2'), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'] + ], + 'bundle_product_1' + ), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price-special-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'], + 'special_price' => 90 // it is the 90% of the original price + ], + 'bundle_product_2' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_1.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_2.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ) + ] + public function testBundleProductFixedPriceWithOptionsWithoutPrices() + { + $cart = $this->fixtures->get('cart'); + $maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int) $cart->getId()); + $query = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + // price is the bundle product price as in this case the options don't have prices + // specialPrice is the bundle product price * bundle product special price % + $expectedResponse = $this->getExpectedResponse(15, 30, 30, 13.5, 27); + + $this->assertEquals($expectedResponse, $response); + } + + #[ + DataFixture(ProductFixture::class, ['price' => 20], 'product1'), + DataFixture(ProductFixture::class, ['price' => 10], 'product2'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$product1.sku$'], 'selection1'), + DataFixture( + BundleSelectionFixture::class, + [ + 'sku' => '$product2.sku$', + 'price' => 10, + 'price_type' => LinkInterface::PRICE_TYPE_FIXED + ], + 'selection2' + ), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection1$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection2$']], 'opt2'), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'], + ], + 'bundle_product_1' + ), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price-special-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'], + 'special_price' => 90 // it is the 90% of the original price + ], + 'bundle_product_2' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_1.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_2.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ) + ] + public function testBundleProductFixedPriceWithOneOptionFixedPrice() + { + $cart = $this->fixtures->get('cart'); + $maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int) $cart->getId()); + $query = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + // price is the bundle product price + option fixed price + // specialPrice is the bundle product price + option fixed price * bundle product special price % + $expectedResponse = $this->getExpectedResponse(25, 50, 50, 22.5, 45); + + $this->assertEquals($expectedResponse, $response); + } + + #[ + DataFixture(ProductFixture::class, ['price' => 20], 'product1'), + DataFixture(ProductFixture::class, ['price' => 10], 'product2'), + DataFixture( + BundleSelectionFixture::class, + [ + 'sku' => '$product1.sku$', + 'price' => 20, + 'price_type' => LinkInterface::PRICE_TYPE_FIXED + ], + 'selection1' + ), + DataFixture( + BundleSelectionFixture::class, + [ + 'sku' => '$product2.sku$', + 'price' => 10, + 'price_type' => LinkInterface::PRICE_TYPE_FIXED + ], + 'selection2' + ), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection1$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection2$']], 'opt2'), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'], + ], + 'bundle_product_1' + ), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price-special-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'], + 'special_price' => 90 // it is the 90% of the original price + ], + 'bundle_product_2' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_1.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_2.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ) + ] + public function testBundleProductFixedPriceWithBothOptionsFixedPrice() + { + $cart = $this->fixtures->get('cart'); + $maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int) $cart->getId()); + $query = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + // price is the bundle product price + options fixed prices + // specialPrice is the bundle product price + options fixed prices * bundle product special price % + $expectedResponse = $this->getExpectedResponse(45, 90, 90, 40.50, 81); + + $this->assertEquals($expectedResponse, $response); + } + + #[ + DataFixture(ProductFixture::class, ['price' => 20], 'product1'), + DataFixture(ProductFixture::class, ['price' => 10], 'product2'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$product1.sku$'], 'selection1'), + DataFixture( + BundleSelectionFixture::class, + [ + 'sku' => '$product2.sku$', + 'price' => 20, + 'price_type' => LinkInterface::PRICE_TYPE_PERCENT + ], + 'selection2' + ), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection1$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection2$']], 'opt2'), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'], + ], + 'bundle_product_1' + ), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price-special-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'], + 'special_price' => 90 // it is the 90% of the original price + ], + 'bundle_product_2' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_1.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_2.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ) + ] + public function testBundleProductFixedPriceWithOneOptionPercentPrice() + { + $cart = $this->fixtures->get('cart'); + $maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int) $cart->getId()); + $query = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + // price is the (bundle product price * option percent price) + bundle product price + // specialPrice is the (bundle product price * option percent price) + + // bundle product price * bundle product special price % + $expectedResponse = $this->getExpectedResponse(18, 36, 36, 16.20, 32.40); + + $this->assertEquals($expectedResponse, $response); + } + + #[ + DataFixture(ProductFixture::class, ['price' => 20], 'product1'), + DataFixture(ProductFixture::class, ['price' => 10], 'product2'), + DataFixture( + BundleSelectionFixture::class, + [ + 'sku' => '$product1.sku$', + 'price' => 10, + 'price_type' => LinkInterface::PRICE_TYPE_PERCENT + ], + 'selection1' + ), + DataFixture( + BundleSelectionFixture::class, + [ + 'sku' => '$product2.sku$', + 'price' => 20, + 'price_type' => LinkInterface::PRICE_TYPE_PERCENT + ], + 'selection2' + ), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection1$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection2$']], 'opt2'), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'], + ], + 'bundle_product_1' + ), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price-special-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'], + 'special_price' => 90 // it is the 90% of the original price + ], + 'bundle_product_2' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_1.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_2.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ) + ] + public function testBundleProductFixedPriceWithBothOptionsPercentPrices() + { + $cart = $this->fixtures->get('cart'); + $maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int) $cart->getId()); + $query = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + // price is the (bundle product price * options percent price) + bundle product price + // specialPrice is the (bundle product price * options percent price) + + // bundle product price * bundle product special price % + $expectedResponse = $this->getExpectedResponse(19.5, 39, 39, 17.55, 35.10); + + $this->assertEquals($expectedResponse, $response); + } + + #[ + DataFixture(ProductFixture::class, ['price' => 20], 'product1'), + DataFixture(ProductFixture::class, ['price' => 10], 'product2'), + DataFixture( + BundleSelectionFixture::class, + [ + 'sku' => '$product1.sku$', + 'price' => 10, + 'price_type' => LinkInterface::PRICE_TYPE_FIXED + ], + 'selection1' + ), + DataFixture( + BundleSelectionFixture::class, + [ + 'sku' => '$product2.sku$', + 'price' => 20, + 'price_type' => LinkInterface::PRICE_TYPE_PERCENT + ], + 'selection2' + ), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection1$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection2$']], 'opt2'), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'], + ], + 'bundle_product_1' + ), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-fixed-price-special-price', + 'price' => 15, + 'price_type' => Price::PRICE_TYPE_FIXED, + '_options' => ['$opt1$', '$opt2$'], + 'special_price' => 90 // it is the 90% of the original price + ], + 'bundle_product_2' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_1.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_2.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ) + ] + public function testBundleProductFixedPriceWithOneOptionFixedAndOnePercentPrice() + { + $cart = $this->fixtures->get('cart'); + $maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int) $cart->getId()); + $query = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + // price is the (bundle product price * option percent price) + bundle product price + option fixed price + // specialPrice is the (bundle product price * option percent price) + bundle product price + + // option fixed price * bundle product special price % + $expectedResponse = $this->getExpectedResponse(28, 56, 56, 25.20, 50.40); + + $this->assertEquals($expectedResponse, $response); + } + + #[ + DataFixture(ProductFixture::class, ['price' => 20], 'product1'), + DataFixture(ProductFixture::class, ['price' => 10], 'product2'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$product1.sku$'], 'selection1'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$product2.sku$'], 'selection2'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection1$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection2$']], 'opt2'), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-dynamic-price', + '_options' => ['$opt1$', '$opt2$'], + ], + 'bundle_product_1' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_1.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ) + ] + public function testBundleProductDynamicPriceWithoutSpecialPrice() + { + $cart = $this->fixtures->get('cart'); + $maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int) $cart->getId()); + $query = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $expectedResponse = [ + "cart" => [ + "items" => [ + 0 => [ + "prices" => [ + "price" => [ + "value" => 30, + "currency" => "USD" + ], + "row_total" => [ + "value" => 60, + "currency" => "USD" + ], + "original_row_total" => [ + "value" => 60, + "currency" => "USD" + ] + ] + ] + ] + ] + ]; + + $this->assertEquals($expectedResponse, $response); + } + + #[ + DataFixture(ProductFixture::class, ['price' => 20, 'special_price' => 15], 'product1'), + DataFixture(ProductFixture::class, ['price' => 10], 'product2'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$product1.sku$'], 'selection1'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$product2.sku$'], 'selection2'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection1$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$selection2$']], 'opt2'), + DataFixture( + BundleProductFixture::class, + [ + 'sku' => 'bundle-product-dynamic-price', + '_options' => ['$opt1$', '$opt2$'], + ], + 'bundle_product_1' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundle_product_1.id$', + 'selections' => [['$product1.id$'], ['$product2.id$']], + 'qty' => 2 + ] + ) + ] + public function testBundleProductDynamicPriceWithSpecialPrice() + { + $cart = $this->fixtures->get('cart'); + $maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int) $cart->getId()); + $query = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $expectedResponse = [ + "cart" => [ + "items" => [ + 0 => [ + "prices" => [ + "price" => [ + "value" => 25, + "currency" => "USD" + ], + "row_total" => [ + "value" => 50, + "currency" => "USD" + ], + "original_row_total" => [ + "value" => 60, + "currency" => "USD" + ] + ] + ] + ] + ] + ]; + + $this->assertEquals($expectedResponse, $response); + } + + /** + * Generates GraphQl query for get cart prices + * + * @param string $maskedQuoteId + * @return string + */ + private function getCartQuery(string $maskedQuoteId): string + { + return << [ + "items" => [ + 0 => [ + "prices" => [ + "price" => [ + "value" => $price, + "currency" => "USD" + ], + "row_total" => [ + "value" => $rowTotal, + "currency" => "USD" + ], + "original_row_total" => [ + "value" => $originalRowTotal, + "currency" => "USD" + ] + ] + ], + 1 => [ + "prices" => [ + "price" => [ + "value" => $specialPrice, + "currency" => "USD" + ], + "row_total" => [ + "value" => $specialRowTotal, + "currency" => "USD" + ], + "original_row_total" => [ + "value" => $originalRowTotal, + "currency" => "USD" + ] + ] + ] + ] + ] + ]; + } +}