diff --git a/app/code/Magento/CatalogRule/Model/Config/CatalogRule.php b/app/code/Magento/CatalogRule/Model/Config/CatalogRule.php deleted file mode 100644 index 4a87a8a851d7..000000000000 --- a/app/code/Magento/CatalogRule/Model/Config/CatalogRule.php +++ /dev/null @@ -1,46 +0,0 @@ -scopeConfig->isSetFlag(self::XML_PATH_SHARE_ALL_CATALOG_RULES); - } - - /** - * Is 'share_applied_catalog_rules' config enabled - * - * @return bool - */ - public function isShareAppliedCatalogRulesEnabled(): bool - { - return $this->scopeConfig->isSetFlag(self::XML_PATH_SHARE_APPLIED_CATALOG_RULES); - } -} diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/GetAllCatalogRules.php b/app/code/Magento/CatalogRule/Model/ResourceModel/GetAllCatalogRules.php deleted file mode 100644 index b9f7744f6201..000000000000 --- a/app/code/Magento/CatalogRule/Model/ResourceModel/GetAllCatalogRules.php +++ /dev/null @@ -1,56 +0,0 @@ -resourceConnection->getConnection(); - $linkField = $this->metadataPool->getMetadata(RuleInterface::class)->getLinkField(); - - return $connection->fetchAll( - $connection->select() - ->from( - ['cr' => $this->resourceConnection->getTableName('catalogrule')], - ['name'] - ) - ->join( - ['crw' => $this->resourceConnection->getTableName('catalogrule_website')], - "cr.rule_id = crw.$linkField", - ) - ->reset('columns') - ->columns(['name']) - ->distinct(true) - ->where('cr.is_active = ?', 1) - ->where('crw.website_id = ?', $websiteId) - ) ?? []; - } -} diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/GetAppliedCatalogRules.php b/app/code/Magento/CatalogRule/Model/ResourceModel/GetAppliedCatalogRules.php deleted file mode 100644 index aa12a1a66de4..000000000000 --- a/app/code/Magento/CatalogRule/Model/ResourceModel/GetAppliedCatalogRules.php +++ /dev/null @@ -1,62 +0,0 @@ -resourceConnection->getConnection(); - $linkField = $this->metadataPool->getMetadata(RuleInterface::class)->getLinkField(); - - return $connection->fetchAll( - $connection->select() - ->from( - ['cr' => $this->resourceConnection->getTableName('catalogrule')], - ['name'] - ) - ->join( - ['crp' => $this->resourceConnection->getTableName('catalogrule_product')], - 'crp.rule_id = cr.rule_id', - ) - ->join( - ['crw' => $this->resourceConnection->getTableName('catalogrule_website')], - "cr.rule_id = crw.$linkField", - ) - ->reset('columns') - ->columns(['name']) - ->distinct(true) - ->where('cr.is_active = ?', 1) - ->where('crp.product_id = ?', $productId) - ->where('crw.website_id = ?', $websiteId) - ) ?? []; - } -} diff --git a/app/code/Magento/CatalogRule/etc/adminhtml/system.xml b/app/code/Magento/CatalogRule/etc/adminhtml/system.xml deleted file mode 100644 index 5a14982e44d8..000000000000 --- a/app/code/Magento/CatalogRule/etc/adminhtml/system.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - -
- - - - - Magento\Config\Model\Config\Source\Yesno - Option to disable providing Catalog Rules details via GraphQl - - - - Magento\Config\Model\Config\Source\Yesno - Option to disable providing Applied Catalog Rules details via GraphQl - - -
-
-
diff --git a/app/code/Magento/CatalogRule/etc/config.xml b/app/code/Magento/CatalogRule/etc/config.xml deleted file mode 100644 index 0a4b6167ba10..000000000000 --- a/app/code/Magento/CatalogRule/etc/config.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - 0 - 1 - - - - diff --git a/app/code/Magento/CatalogRuleGraphQl/Model/Resolver/AllCatalogRules.php b/app/code/Magento/CatalogRuleGraphQl/Model/Resolver/AllCatalogRules.php deleted file mode 100644 index 2032d0fd1d99..000000000000 --- a/app/code/Magento/CatalogRuleGraphQl/Model/Resolver/AllCatalogRules.php +++ /dev/null @@ -1,55 +0,0 @@ -config->isShareAllCatalogRulesEnabled()) { - throw new GraphQlInputException(__('Sharing catalog rules information is disabled or not configured.')); - } - - return array_map( - fn ($rule) => ['name' => $rule['name']], - $this->getAllCatalogRules->execute( - (int)$context->getExtensionAttributes()->getStore()->getWebsiteId() - ) - ); - } -} diff --git a/app/code/Magento/CatalogRuleGraphQl/Model/Resolver/AppliedCatalogRules.php b/app/code/Magento/CatalogRuleGraphQl/Model/Resolver/AppliedCatalogRules.php deleted file mode 100644 index aee83683af0e..000000000000 --- a/app/code/Magento/CatalogRuleGraphQl/Model/Resolver/AppliedCatalogRules.php +++ /dev/null @@ -1,60 +0,0 @@ -config->isShareAppliedCatalogRulesEnabled()) { - return null; //Returning `null` to ensure that the entire product response remains intact. - } - - return array_map( - fn ($rule) => ['name' => $rule['name']], - $this->getAppliedCatalogRules->execute( - (int)$value['model']->getId(), - (int)$context->getExtensionAttributes()->getStore()->getWebsiteId() - ) - ); - } -} diff --git a/app/code/Magento/CatalogRuleGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogRuleGraphQl/etc/graphql/di.xml index 1781e71feeee..40290d33789a 100644 --- a/app/code/Magento/CatalogRuleGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogRuleGraphQl/etc/graphql/di.xml @@ -9,12 +9,4 @@ - - - - catalog/rule/share_all_catalog_rules - catalog/rule/share_applied_catalog_rules - - - diff --git a/app/code/Magento/CatalogRuleGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogRuleGraphQl/etc/schema.graphqls deleted file mode 100644 index fd445c5e2736..000000000000 --- a/app/code/Magento/CatalogRuleGraphQl/etc/schema.graphqls +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2025 Adobe -# All Rights Reserved. - -type StoreConfig { - share_all_catalog_rules: Boolean! @doc(description: "Configuration data from catalog/rule/share_all_catalog_rules") - share_applied_catalog_rules: Boolean! @doc(description: "Configuration data from catalog/rule/share_applied_catalog_rules") -} - -type Query { - allCatalogRules: [CatalogRule!] @doc(description: "Provides all active catalog rules in the store.") @resolver(class: "Magento\\CatalogRuleGraphQl\\Model\\Resolver\\AllCatalogRules") -} - -interface ProductInterface { - rules: [CatalogRule!] @doc(description: "Provides applied catalog rules in the current active cart") @resolver(class: "Magento\\CatalogRuleGraphQl\\Model\\Resolver\\AppliedCatalogRules") -} - -type CatalogRule { - name: String! @doc(description: "Name of the catalog rule") -} diff --git a/app/code/Magento/Customer/Model/Config/AccountInformation.php b/app/code/Magento/Customer/Model/Config/AccountInformation.php index 3120a3123a55..2f6a21f62334 100644 --- a/app/code/Magento/Customer/Model/Config/AccountInformation.php +++ b/app/code/Magento/Customer/Model/Config/AccountInformation.php @@ -11,7 +11,6 @@ class AccountInformation { - private const XML_PATH_SHARE_ALL_CUSTOMER_GROUPS = 'customer/account_information/graphql_share_all_customer_groups'; private const XML_PATH_SHARE_CUSTOMER_GROUP = 'customer/account_information/graphql_share_customer_group'; /** @@ -24,16 +23,6 @@ public function __construct( ) { } - /** - * Is 'graphql_share_all_customer_groups' config enabled - * - * @return bool - */ - public function isShareAllCustomerGroupsEnabled(): bool - { - return $this->scopeConfig->isSetFlag(self::XML_PATH_SHARE_ALL_CUSTOMER_GROUPS); - } - /** * Is 'graphql_share_customer_group' config enabled * diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml index c260cb6572ac..df9d7fbc4918 100644 --- a/app/code/Magento/Customer/etc/adminhtml/system.xml +++ b/app/code/Magento/Customer/etc/adminhtml/system.xml @@ -197,15 +197,10 @@ Magento\Config\Model\Config\Source\Yesno - - - Magento\Config\Model\Config\Source\Yesno - Option to disable providing all customer group details via GraphQl - - + Magento\Config\Model\Config\Source\Yesno - Option to disable providing customer group details via GraphQl for a customer + Option to disable providing customer group details via GraphQl diff --git a/app/code/Magento/Customer/etc/config.xml b/app/code/Magento/Customer/etc/config.xml index ca7d4f1c3cea..8b385133298b 100644 --- a/app/code/Magento/Customer/etc/config.xml +++ b/app/code/Magento/Customer/etc/config.xml @@ -33,8 +33,7 @@ customer_account_information_change_email_template customer_account_information_change_email_and_password_template 0 - 0 - 0 + 1 support diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/AllCustomerGroups.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/AllCustomerGroups.php deleted file mode 100644 index 2d44c2071666..000000000000 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/AllCustomerGroups.php +++ /dev/null @@ -1,71 +0,0 @@ -config->isShareAllCustomerGroupsEnabled()) { - throw new GraphQlInputException(__('Sharing customer group information is disabled or not configured.')); - } - - try { - $customerGroups = $this->groupRepository->getList( - $this->searchCriteriaBuilder->create() - )->getItems(); - } catch (Exception $e) { - throw new GraphQlInputException(__('Unable to retrieve customer groups.')); - } - - return array_map( - fn ($group) => ['name' => $group->getCode()], - array_filter( - $customerGroups, - fn ($group) => !in_array( - (int)$context->getExtensionAttributes()->getStore()->getWebsiteId(), - $group->getExtensionAttributes()->getExcludeWebsiteIds() ?? [] - ) - ) - ); - } -} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerGroup.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerGroup.php index 007de18ba9b1..e1ddd04e2044 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerGroup.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerGroup.php @@ -7,28 +7,24 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Config\AccountInformation; -use Magento\CustomerGraphQl\Model\GetCustomerGroupName; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -/** - * Provides data for customer.group.name - */ class CustomerGroup implements ResolverInterface { /** * CustomerGroup Constructor * * @param AccountInformation $config - * @param GetCustomerGroupName $getCustomerGroup + * @param Uid $idEncoder */ public function __construct( - private readonly AccountInformation $config, - private readonly GetCustomerGroupName $getCustomerGroup + private readonly AccountInformation $config, + private readonly Uid $idEncoder ) { } @@ -42,14 +38,16 @@ public function resolve( ?array $value = null, ?array $args = null ): ?array { - if (!($value['model'] ?? null) instanceof CustomerInterface) { + if (!isset($value['model'])) { throw new LocalizedException(__('"model" value should be specified')); } - return !$this->config->isShareCustomerGroupEnabled() ? null : - $this->getCustomerGroup->execute( - (int) $value['model']->getGroupId(), - (int) $context->getExtensionAttributes()->getStore()->getWebsiteId() - ); + if (!$this->config->isShareCustomerGroupEnabled()) { + return null; + } + + return [ + 'uid' => $this->idEncoder->encode((string)$value['model']->getGroupId()) + ]; } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/GetCustomerGroup.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Visitor.php similarity index 58% rename from app/code/Magento/CustomerGraphQl/Model/Resolver/GetCustomerGroup.php rename to app/code/Magento/CustomerGraphQl/Model/Resolver/Visitor.php index f84bbb1abe5a..236fa4677c4d 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/GetCustomerGroup.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Visitor.php @@ -7,30 +7,27 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\CatalogCustomerGraphQl\Model\Resolver\Customer\GetCustomerGroup as CustomerGroup; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Customer\GetCustomerGroup; use Magento\Customer\Model\Config\AccountInformation; -use Magento\CustomerGraphQl\Model\GetCustomerGroupName; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -/** - * Provides data for customerGroup.name - */ -class GetCustomerGroup implements ResolverInterface +class Visitor implements ResolverInterface { /** - * GetCustomerGroup Constructor + * Visitor Constructor * * @param AccountInformation $config - * @param CustomerGroup $customerGroup - * @param GetCustomerGroupName $getCustomerGroup + * @param GetCustomerGroup $getCustomerGroup + * @param Uid $idEncoder */ public function __construct( - private readonly AccountInformation $config, - private readonly CustomerGroup $customerGroup, - private readonly GetCustomerGroupName $getCustomerGroup + private readonly AccountInformation $config, + private readonly GetCustomerGroup $getCustomerGroup, + private readonly Uid $idEncoder ) { } @@ -44,13 +41,13 @@ public function resolve( ?array $value = null, ?array $args = null ): array { + if (!$this->config->isShareCustomerGroupEnabled()) { throw new GraphQlInputException(__('Sharing customer group information is disabled or not configured.')); } - return $this->getCustomerGroup->execute( - $this->customerGroup->execute($context->getUserId()), - (int)$context->getExtensionAttributes()->getStore()->getWebsiteId() - ); + return [ + 'uid' => $this->idEncoder->encode((string)$this->getCustomerGroup->execute($context->getUserId())) + ]; } } diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml index d8db12736124..779ccc4b91fd 100644 --- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml @@ -38,7 +38,6 @@ customer/password/minimum_password_length customer/password/autocomplete_on_storefront customer/create_account/confirm - customer/account_information/graphql_share_all_customer_groups customer/account_information/graphql_share_customer_group diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index abb8c968b6d1..667eecbfe9d3 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -6,8 +6,7 @@ type StoreConfig { minimum_password_length : String @doc(description: "The minimum number of characters required for a valid password.") autocomplete_on_storefront : Boolean @doc(description: "Indicates whether to enable autocomplete on login and forgot password forms.") create_account_confirmation: Boolean @doc(description: "Indicates if the new accounts need confirmation.") - graphql_share_all_customer_groups: Boolean! @doc(description: "Configuration data from customer/account_information/graphql_share_all_customer_groups") - graphql_share_customer_group: Boolean! @doc(description: "Configuration data from customer/account_information/graphql_share_customer_group") + graphql_share_customer_group: Boolean @doc(description: "Configuration data from customer/account_information/graphql_share_customer_group") } type Query { @@ -15,8 +14,7 @@ type Query { isEmailAvailable ( email: String! @doc(description: "The email address to check.") ): IsEmailAvailableOutput @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\IsEmailAvailable") @doc(description: "Check whether the specified email has already been used to create a customer account.") - allCustomerGroups: [CustomerGroup!] @doc(description: "An array containing a list of all Customer Groups available.") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\AllCustomerGroups") - customerGroup: CustomerGroup! @doc(description: "Provides name of the Customer Group assigned to the Customer or Guest.") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\GetCustomerGroup") + customerGroup: CustomerGroupStorefront! @doc(description: "Provides Customer Group assigned to the Customer or Guest.") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\Visitor") } type Mutation { @@ -157,7 +155,7 @@ type Customer @doc(description: "Defines the customer name, addresses, and other gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2).") custom_attributes(attributeCodes: [ID!]): [AttributeValueInterface] @doc(description: "Customer's custom attributes.") @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CustomAttributeFilter") confirmation_status: ConfirmationStatusEnum! @doc(description: "The customer's confirmation status.") @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\ConfirmationStatus") - group: CustomerGroup @doc(description: "Name of the customer group assigned to the customer") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerGroup") + group: CustomerGroupStorefront @doc(description: "Customer group assigned to the customer") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerGroup") } type CustomerAddresses { @@ -497,6 +495,6 @@ enum ConfirmationStatusEnum @doc(description: "List of account confirmation stat ACCOUNT_CONFIRMATION_NOT_REQUIRED @doc(description: "Account confirmation not required") } -type CustomerGroup @doc(description: "Data of customer group.") { - name: String @doc(description: "The name of customer group.") +type CustomerGroupStorefront @doc(description: "Data of customer group.") { + uid: ID! @doc(description: "The unique ID for a `CustomerGroup` object.") } diff --git a/app/code/Magento/Quote/etc/di.xml b/app/code/Magento/Quote/etc/di.xml index e53ce4c7c0ec..ee17a1b8dd5b 100644 --- a/app/code/Magento/Quote/etc/di.xml +++ b/app/code/Magento/Quote/etc/di.xml @@ -182,6 +182,7 @@ INSUFFICIENT_STOCK INSUFFICIENT_STOCK INSUFFICIENT_STOCK + REQUIRED_PARAMETER_MISSING diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php index bd3eba6cdda2..4854675629d9 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogInventory\Api\Data\StockStatusInterface; use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\CatalogInventory\Model\Configuration; use Magento\CatalogInventory\Model\StockState; @@ -18,7 +19,7 @@ use Magento\Store\Model\ScopeInterface; /** - * Product Stock class to check availability of product + * Product Stock class to check the availability of product */ class ProductStock { @@ -33,7 +34,7 @@ class ProductStock private const PRODUCT_TYPE_CONFIGURABLE = "configurable"; /** - * ProductStock constructor + * ProductStock Constructor * * @param ProductRepositoryInterface $productRepositoryInterface * @param StockState $stockState @@ -57,8 +58,8 @@ public function __construct( */ public function isProductAvailable(Item $cartItem): bool { - $requestedQty = $cartItem->getQtyToAdd() ?? $cartItem->getQty(); - $previousQty = $cartItem->getPreviousQty() ?? 0; + $requestedQty = (float)($cartItem->getQtyToAdd() ?? $cartItem->getQty()); + $previousQty = (int)$cartItem->getPreviousQty() ?? 0; if ($cartItem->getProductType() === self::PRODUCT_TYPE_BUNDLE) { return $this->isStockAvailableBundle($cartItem, $previousQty, $requestedQty); @@ -98,7 +99,7 @@ public function isStockAvailableBundle(Item $cartItem, int $previousQty, $reques $qtyOptions = $cartItem->getQtyOptions(); $totalRequestedQty = $previousQty + $requestedQty; foreach ($qtyOptions as $qtyOption) { - $requiredItemQty = $qtyOption->getValue(); + $requiredItemQty = (float)$qtyOption->getValue(); if ($totalRequestedQty) { $requiredItemQty = $requiredItemQty * $totalRequestedQty; } @@ -176,16 +177,16 @@ private function isStockQtyAvailable( float $requiredQuantity, float $prevQty ): bool { - $scopeId = $cartItem->getStore()->getId(); - $stockStatus = $this->stockState->checkQuoteItemQty( + $this->stockState->checkQuoteItemQty( $product->getId(), $itemQty, $requiredQuantity, $prevQty, - $scopeId + $cartItem->getStore()->getId() ); - return ((bool) $stockStatus->getHasError()) === false; + return ($this->getProductStockStatus($product)->getStockStatus() && + $this->getAvailableStock($product) >= $itemQty); } /** @@ -196,7 +197,7 @@ private function isStockQtyAvailable( */ private function getAvailableStock(ProductInterface $product): float { - return $this->stockState->getStockQty($product->getId()); + return (float) $this->getProductStockStatus($product)->getQty(); } /** @@ -293,4 +294,18 @@ public function getSaleableQty(ProductInterface $product, ?float $thresholdQty): return ($stockQty >= 0 && $stockLeft <= $thresholdQty) ? $stockQty : 0.0; } + + /** + * Returns the stock status of a product + * + * @param ProductInterface $product + * @return StockStatusInterface + */ + private function getProductStockStatus(ProductInterface $product): StockStatusInterface + { + return $this->stockRegistry->getStockStatus( + $product->getId(), + $product->getStore()->getWebsiteId() + ); + } } diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/CartItem/ProductStockTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/CartItem/ProductStockTest.php index 2b6737943f4e..6b7bc3335b83 100644 --- a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/CartItem/ProductStockTest.php +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/CartItem/ProductStockTest.php @@ -70,6 +70,16 @@ class ProductStockTest extends TestCase */ private $stockStatusMock; + /** + * @var ProductInterface|MockObject + */ + private $optionProductMock; + + /** + * @var Option|MockObject + */ + private $qtyOptionMock; + /** * Set up mocks and initialize the ProductStock class */ @@ -87,15 +97,25 @@ protected function setUp(): void ); $this->stockStatusMock = $this->getMockBuilder(StockStatusInterface::class) ->disableOriginalConstructor() - ->addMethods(['getHasError']) + ->onlyMethods(['getQty', 'getStockStatus']) ->getMockForAbstractClass(); $this->cartItemMock = $this->getMockBuilder(Item::class) ->addMethods(['getQtyToAdd', 'getPreviousQty']) ->onlyMethods(['getStore', 'getProductType', 'getProduct', 'getChildren', 'getQtyOptions']) ->disableOriginalConstructor() ->getMock(); - $this->productMock = $this->createMock(ProductInterface::class); + $this->productMock = $this->getMockBuilder(ProductInterface::class) + ->onlyMethods(['getId']) + ->addMethods(['getStore']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->optionProductMock = $this->getMockBuilder(ProductInterface::class) + ->onlyMethods(['getId']) + ->addMethods(['getStore']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->storeMock = $this->createMock(StoreInterface::class); + $this->qtyOptionMock = $this->createMock(Option::class); } /** @@ -121,16 +141,25 @@ public function testIsProductAvailableForSimpleProductWithStock(): void $this->storeMock->expects($this->once()) ->method('getId') ->willReturn(1); - $this->productMock->expects($this->once()) + $this->productMock->expects($this->exactly(3)) ->method('getId') ->willReturn(123); + $this->productMock->expects($this->exactly(2)) + ->method('getStore') + ->willReturn($this->storeMock); $this->stockStatusMock->expects($this->once()) - ->method('getHasError') - ->willReturn(false); + ->method('getStockStatus') + ->willReturn(true); + $this->stockStatusMock->expects($this->once()) + ->method('getQty') + ->willReturn(10); $this->stockStateMock->expects($this->once()) ->method('checkQuoteItemQty') ->with(123, 2.0, 3.0, 1.0, 1) ->willReturn($this->stockStatusMock); + $this->stockRegistryMock->expects($this->exactly(2)) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); $this->cartItemMock->expects($this->never())->method('getChildren'); $result = $this->productStock->isProductAvailable($this->cartItemMock); $this->assertTrue($result); @@ -159,16 +188,22 @@ public function testIsProductAvailableForSimpleProductWithoutStock() $this->storeMock->expects($this->once()) ->method('getId') ->willReturn(1); - $this->productMock->expects($this->once()) + $this->productMock->expects($this->exactly(2)) ->method('getId') ->willReturn(123); + $this->productMock->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); $this->stockStateMock->expects($this->once()) ->method('checkQuoteItemQty') ->with(123, 2.0, 3.0, 1.0, 1) ->willReturn($this->stockStatusMock); $this->stockStatusMock->expects($this->once()) - ->method('getHasError') - ->willReturn(true); + ->method('getStockStatus') + ->willReturn(false); + $this->stockRegistryMock->expects($this->once()) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); $this->cartItemMock->expects($this->never())->method('getChildren'); $result = $this->productStock->isProductAvailable($this->cartItemMock); $this->assertFalse($result); @@ -179,33 +214,40 @@ public function testIsProductAvailableForSimpleProductWithoutStock() */ public function testIsStockAvailableBundleStockAvailable() { - $qtyOptionMock = $this->createMock(Option::class); - $qtyOptionMock->expects($this->once()) + $this->qtyOptionMock->expects($this->once()) ->method('getValue') - ->willReturn(2.0); - $optionProductMock = $this->createMock(ProductInterface::class); - $qtyOptionMock->expects($this->once()) + ->willReturn(1.0); + $this->qtyOptionMock->expects($this->once()) ->method('getProduct') - ->willReturn($optionProductMock); + ->willReturn($this->optionProductMock); $this->cartItemMock->expects($this->once()) ->method('getQtyOptions') - ->willReturn([$qtyOptionMock]); + ->willReturn([$this->qtyOptionMock]); $this->cartItemMock->expects($this->once()) ->method('getStore') ->willReturn($this->storeMock); $this->storeMock->expects($this->once()) ->method('getId') ->willReturn(1); - $optionProductMock->expects($this->once()) + $this->optionProductMock->expects($this->exactly(3)) ->method('getId') ->willReturn(789); - $this->stockStatusMock->expects($this->once()) - ->method('getHasError') - ->willReturn(false); + $this->optionProductMock->expects($this->exactly(2)) + ->method('getStore') + ->willReturn($this->storeMock); $this->stockStateMock->expects($this->once()) ->method('checkQuoteItemQty') - ->with(789, 2.0, 6.0, 1.0, 1) + ->with(789, 2.0, 3.0, 1.0, 1) ->willReturn($this->stockStatusMock); + $this->stockStatusMock->expects($this->once()) + ->method('getStockStatus') + ->willReturn(true); + $this->stockRegistryMock->expects($this->exactly(2)) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); + $this->stockStatusMock->expects($this->once()) + ->method('getQty') + ->willReturn(10); $result = $this->productStock->isStockAvailableBundle($this->cartItemMock, 1, 2.0); $this->assertTrue($result); } @@ -215,33 +257,37 @@ public function testIsStockAvailableBundleStockAvailable() */ public function testIsStockAvailableBundleStockNotAvailable() { - $qtyOptionMock = $this->createMock(\Magento\Quote\Model\Quote\Item\Option::class); - $qtyOptionMock->expects($this->once()) + $this->qtyOptionMock->expects($this->once()) ->method('getValue') ->willReturn(2.0); - $optionProductMock = $this->createMock(ProductInterface::class); - $qtyOptionMock->expects($this->once()) + $this->qtyOptionMock->expects($this->once()) ->method('getProduct') - ->willReturn($optionProductMock); + ->willReturn($this->optionProductMock); $this->cartItemMock->expects($this->once()) ->method('getQtyOptions') - ->willReturn([$qtyOptionMock]); + ->willReturn([$this->qtyOptionMock]); $this->cartItemMock->expects($this->once()) ->method('getStore') ->willReturn($this->storeMock); $this->storeMock->expects($this->once()) ->method('getId') ->willReturn(1); - $this->stockStatusMock->expects($this->once()) - ->method('getHasError') - ->willReturn(true); - $optionProductMock->expects($this->once()) + $this->optionProductMock->expects($this->exactly(2)) ->method('getId') ->willReturn(789); + $this->optionProductMock->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); $this->stockStateMock->expects($this->once()) ->method('checkQuoteItemQty') ->with(789, 2.0, 6.0, 1.0, 1) ->willReturn($this->stockStatusMock); + $this->stockStatusMock->expects($this->once()) + ->method('getStockStatus') + ->willReturn(false); + $this->stockRegistryMock->expects($this->once()) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); $result = $this->productStock->isStockAvailableBundle($this->cartItemMock, 1, 2.0); $this->assertFalse($result); } diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php index ca6ddf804f0d..6c281e50d96a 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php @@ -55,6 +55,7 @@ public function resolve( 'currency' => $order->getBaseCurrencyCode() ], 'grand_total' => ['value' => $order->getGrandTotal(), 'currency' => $currency], + 'grand_total_excl_tax' => ['value' => $this->getGrandTotalExclTax($order), 'currency' => $currency], 'subtotal' => ['value' => $order->getSubtotal(), 'currency' => $currency], 'subtotal_incl_tax' => ['value' => $order->getSubtotalInclTax(), 'currency' => $currency], 'subtotal_excl_tax' => ['value' => $order->getSubtotal(), 'currency' => $currency], @@ -217,4 +218,17 @@ private function getShippingDiscountDetails(OrderInterface $order): array } return $shippingDiscounts; } + + /** + * Get grand total excluding tax + * + * @param OrderInterface $order + * @return float + */ + private function getGrandTotalExclTax(OrderInterface $order): float + { + return (float) ($order->getSubtotal() + + $order->getShippingAmount() + - abs((float)$order->getDiscountAmount())); + } } diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/ProductResolver.php b/app/code/Magento/SalesGraphQl/Model/Resolver/ProductResolver.php index 665d4d917cb4..1d10e3a6756e 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/ProductResolver.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/ProductResolver.php @@ -7,12 +7,12 @@ namespace Magento\SalesGraphQl\Model\Resolver; -use Magento\Catalog\Model\Product; use Magento\CatalogGraphQl\Model\ProductDataProvider; +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; -use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Sales\Api\Data\OrderItemInterface; /** * Fetches the Product data according to the GraphQL schema @@ -39,14 +39,10 @@ public function resolve( ?array $value = null, ?array $args = null ) { - if (!isset($value['associatedProduct'])) { - throw new GraphQlNoSuchEntityException( - __("This product is currently out of stock or not available.") - ); + if (!(($value['model'] ?? null) instanceof OrderItemInterface)) { + throw new LocalizedException(__('"model" value should be specified')); } - /** @var Product $product */ - $product = $value['associatedProduct']; - return $this->productDataProvider->getProductDataById((int)$product->getId()); + return $this->productDataProvider->getProductDataById((int)$value['model']->getProductId()); } } diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls index ec33e4986d59..58c62b2814c0 100644 --- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -3,7 +3,7 @@ type Query { customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @deprecated(reason: "Use the `customer` query instead.") @cache(cacheable: false) - guestOrder(input: OrderInformationInput!): CustomerOrder! @doc(description:"Retrieve guest order details based on number, email and billing last name.") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\GuestOrder") @cache(cacheable: false) + guestOrder(input: GuestOrderInformationInput!): CustomerOrder! @doc(description:"Retrieve guest order details based on number, email and billing last name.") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\GuestOrder") @cache(cacheable: false) guestOrderByToken(input: OrderTokenInput!): CustomerOrder! @doc(description:"Retrieve guest order details based on token.") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\GuestOrder") @cache(cacheable: false) } @@ -170,6 +170,7 @@ type OrderTotal @doc(description: "Contains details about the sales total amount shipping_handling: ShippingHandling @doc(description: "Details about the shipping and handling costs for the order.") subtotal_incl_tax: Money! @doc(description: "The subtotal of the order, including taxes.") subtotal_excl_tax: Money! @doc(description: "The subtotal of the order, excluding taxes.") + grand_total_excl_tax: Money! @doc(description: "The grand total of the order, excluding taxes.") } type Invoice @doc(description: "Contains invoice details.") { @@ -309,7 +310,14 @@ input OrderTokenInput @doc(description: "Input to retrieve an order based on tok token: String! @doc(description: "Order token.") } -input OrderInformationInput @doc(description: "Input to retrieve an order based on details.") { +input OrderInformationInput @deprecated(reason: "Use `GuestOrderInformationInput` instead of OrderInformationInput.") @doc(description: "Input to retrieve an order based on details.") { + number: String! @doc(description: "Order number.") + email: String! @doc(description: "Order billing address email.") + lastname: String! @doc(description: "Order billing address lastname.") + postcode: String @deprecated(reason: "Use lastname instead of postcode") @doc(description: "Order billing address postcode") +} + +input GuestOrderInformationInput @doc(description: "Input to retrieve an order based on details.") { number: String! @doc(description: "Order number.") email: String! @doc(description: "Order billing address email.") lastname: String! @doc(description: "Order billing address lastname.") diff --git a/app/code/Magento/SalesRule/Model/Config.php b/app/code/Magento/SalesRule/Model/Config.php new file mode 100644 index 000000000000..59994bdf0574 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Config.php @@ -0,0 +1,36 @@ +scopeConfig->isSetFlag(self::SHARE_APPLIED_CART_RULES, ScopeInterface::SCOPE_STORE); + } +} diff --git a/app/code/Magento/SalesRule/Model/Config/Coupon.php b/app/code/Magento/SalesRule/Model/Config/Coupon.php deleted file mode 100644 index c3f0b9dbf843..000000000000 --- a/app/code/Magento/SalesRule/Model/Config/Coupon.php +++ /dev/null @@ -1,53 +0,0 @@ -scopeConfig->isSetFlag( - self::XML_PATH_PROMO_GRAPHQL_SHARE_ALL_RULES, - ScopeInterface::SCOPE_STORE - ); - } - - /** - * Get share currently applied sales rule flag value - * - * @return bool - */ - public function isShareAppliedSalesRulesEnabled(): bool - { - return $this->scopeConfig->isSetFlag( - self::XML_PATH_PROMO_GRAPHQL_SHARE_APPLIED_RULES, - ScopeInterface::SCOPE_STORE - ); - } -} diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/GetAllCartRules.php b/app/code/Magento/SalesRule/Model/ResourceModel/GetAllCartRules.php deleted file mode 100644 index 36472aebc01b..000000000000 --- a/app/code/Magento/SalesRule/Model/ResourceModel/GetAllCartRules.php +++ /dev/null @@ -1,59 +0,0 @@ -resourceConnection->getConnection(); - $linkField = $this->metadataPool->getMetadata(RuleInterface::class)->getLinkField(); - - return $connection->fetchAll( - $connection->select() - ->from(['sr' => $this->resourceConnection->getTableName('salesrule')]) - ->reset('columns') - ->columns(['name']) - ->join( - ['srw' => $this->resourceConnection->getTableName('salesrule_website')], - "sr.rule_id = srw.$linkField", - [] - ) - ->where('sr.is_active = ?', 1) - ->where( - 'srw.website_id = ?', - (int)$context->getExtensionAttributes()->getStore()->getWebsiteId() - ) - ) ?? []; - } -} diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/GetAppliedCartRules.php b/app/code/Magento/SalesRule/Model/ResourceModel/GetAppliedCartRules.php deleted file mode 100644 index ac28257b1400..000000000000 --- a/app/code/Magento/SalesRule/Model/ResourceModel/GetAppliedCartRules.php +++ /dev/null @@ -1,61 +0,0 @@ -resourceConnection->getConnection(); - $linkField = $this->metadataPool->getMetadata(RuleInterface::class)->getLinkField(); - - return $connection->fetchAll( - $connection->select() - ->from(['sr' => $this->resourceConnection->getTableName('salesrule')]) - ->reset('columns') - ->columns(['name']) - ->join( - ['srw' => $this->resourceConnection->getTableName('salesrule_website')], - "sr.rule_id = srw.$linkField", - [] - ) - ->where('sr.is_active = ?', 1) - ->where('sr.rule_id IN (?)', explode(',', $ruleIds)) - ->where( - 'srw.website_id = ?', - (int)$context->getExtensionAttributes()->getStore()->getWebsiteId() - ) - ) ?? []; - } -} diff --git a/app/code/Magento/SalesRule/etc/adminhtml/system.xml b/app/code/Magento/SalesRule/etc/adminhtml/system.xml index 7dc6e1f9278d..cdfa56565158 100755 --- a/app/code/Magento/SalesRule/etc/adminhtml/system.xml +++ b/app/code/Magento/SalesRule/etc/adminhtml/system.xml @@ -40,17 +40,12 @@ validate-digits - + - - + + Magento\Config\Model\Config\Source\Yesno - Configuration to allow to disable providing all cart rules via GraphQL - - - - Magento\Config\Model\Config\Source\Yesno - Configuration to allow to disable currently applied sales rules for cart via GraphQL + Allow to retrieve applied cart rules via GraphQL API diff --git a/app/code/Magento/SalesRule/etc/config.xml b/app/code/Magento/SalesRule/etc/config.xml index aa1c453ef398..c3593558e3b8 100755 --- a/app/code/Magento/SalesRule/etc/config.xml +++ b/app/code/Magento/SalesRule/etc/config.xml @@ -14,8 +14,7 @@ 1 - 0 - 1 + 1 diff --git a/app/code/Magento/SalesRuleGraphQl/Model/Resolver/AllCartRules.php b/app/code/Magento/SalesRuleGraphQl/Model/Resolver/AllCartRules.php deleted file mode 100644 index 6ae43d35c556..000000000000 --- a/app/code/Magento/SalesRuleGraphQl/Model/Resolver/AllCartRules.php +++ /dev/null @@ -1,50 +0,0 @@ -config->isShareAllSalesRulesEnabled()) { - throw new GraphQlInputException(__('Sharing Cart Rules information is disabled or not configured.')); - } - - return array_map( - static fn ($rule) => ['name' => $rule['name']], - $this->getAllCartRules->execute($context) - ); - } -} diff --git a/app/code/Magento/SalesRuleGraphQl/Model/Resolver/AppliedCartRules.php b/app/code/Magento/SalesRuleGraphQl/Model/Resolver/AppliedCartRules.php index 106e77f4cf3f..eca77dd20998 100644 --- a/app/code/Magento/SalesRuleGraphQl/Model/Resolver/AppliedCartRules.php +++ b/app/code/Magento/SalesRuleGraphQl/Model/Resolver/AppliedCartRules.php @@ -10,22 +10,24 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Quote\Api\Data\CartInterface; -use Magento\SalesRule\Model\Config\Coupon; -use Magento\SalesRule\Model\ResourceModel\GetAppliedCartRules; +use Magento\SalesRule\Model\Config; +/** + * Resolver class for providing All applied cart rules + */ class AppliedCartRules implements ResolverInterface { /** * AppliedCartRules Constructor * - * @param Coupon $config - * @param GetAppliedCartRules $getAppliedCartRules + * @param Config $config + * @param Uid $idEncoder */ public function __construct( - private readonly Coupon $config, - private readonly GetAppliedCartRules $getAppliedCartRules + private readonly Config $config, + private readonly Uid $idEncoder ) { } @@ -38,20 +40,20 @@ public function resolve( ResolveInfo $info, ?array $value = null, ?array $args = null - ) { - if (!(($value['model'] ?? null) instanceof CartInterface)) { + ): ?array { + if (empty($value['model'])) { throw new LocalizedException(__('"model" value should be specified')); } - if (!$this->config->isShareAppliedSalesRulesEnabled()) { - return null; //returning null so that whole cart response is not broken + if (!$this->config->isShareAppliedCartRulesEnabled()) { + return null; } $ruleIds = $value['model']->getAppliedRuleIds(); return $ruleIds ? array_map( - fn ($rule) => ['name' => $rule['name']], - $this->getAppliedCartRules->execute($ruleIds, $context) + fn ($rule) => ['uid' => $this->idEncoder->encode($rule)], + explode(",", $ruleIds) ) : []; } } diff --git a/app/code/Magento/SalesRuleGraphQl/composer.json b/app/code/Magento/SalesRuleGraphQl/composer.json index 8f5ba0cb0743..f566b5f84e2f 100644 --- a/app/code/Magento/SalesRuleGraphQl/composer.json +++ b/app/code/Magento/SalesRuleGraphQl/composer.json @@ -7,8 +7,7 @@ "require": { "php": "~8.2.0||~8.3.0||~8.4.0", "magento/framework": "*", - "magento/module-sales-rule": "*", - "magento/module-quote": "*" + "magento/module-sales-rule": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/SalesRuleGraphQl/etc/graphql/di.xml b/app/code/Magento/SalesRuleGraphQl/etc/graphql/di.xml index 066640e3e463..e55b13dec937 100644 --- a/app/code/Magento/SalesRuleGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/SalesRuleGraphQl/etc/graphql/di.xml @@ -9,8 +9,7 @@ - promo/graphql/share_all_sales_rule - promo/graphql/share_applied_sales_rule + promo/graphql/share_applied_cart_rule diff --git a/app/code/Magento/SalesRuleGraphQl/etc/schema.graphqls b/app/code/Magento/SalesRuleGraphQl/etc/schema.graphqls index 6bac9c7767c1..ea83f4aad0c4 100644 --- a/app/code/Magento/SalesRuleGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesRuleGraphQl/etc/schema.graphqls @@ -1,24 +1,18 @@ # Copyright 2024 Adobe # All Rights Reserved. - type Discount { coupon: AppliedCoupon @resolver(class: "Magento\\SalesRuleGraphQl\\Model\\Resolver\\Coupon") @doc(description:"The coupon related to the discount.") } type StoreConfig { - share_all_sales_rule: Boolean! @doc(description: "Configuration data from promo/graphql/share_all_sales_rule") - share_applied_sales_rule: Boolean! @doc(description: "Configuration data from promo/graphql/share_applied_sales_rule") -} - -type Query { - allCartRules: [CartRule!] @doc(description: "Provides all active cart rules in the store.") @resolver(class: "Magento\\SalesRuleGraphQl\\Model\\Resolver\\AllCartRules") + share_applied_cart_rule: Boolean @doc(description: "Configuration data from promo/graphql/share_applied_cart_rule") } type Cart { - rules: [CartRule!] @doc(description: "Provides applied cart rules in the current active cart") @resolver(class: "Magento\\SalesRuleGraphQl\\Model\\Resolver\\AppliedCartRules") + rules: [CartRuleStorefront!] @doc(description: "Provides applied cart rules in the current active cart") @resolver(class: "Magento\\SalesRuleGraphQl\\Model\\Resolver\\AppliedCartRules") } -type CartRule { - name: String! @doc(description: "Name of the cart price rule") +type CartRuleStorefront { + uid: ID! @doc(description: "The unique ID for a `CartRule` object.") } diff --git a/app/code/Magento/Tax/Test/Fixture/TaxRate.php b/app/code/Magento/Tax/Test/Fixture/TaxRate.php index f79e2ba2a28a..d086abe80c06 100644 --- a/app/code/Magento/Tax/Test/Fixture/TaxRate.php +++ b/app/code/Magento/Tax/Test/Fixture/TaxRate.php @@ -1,7 +1,7 @@ null, 'zip_from' => null, 'zip_to' => null, - 'titles' => [], + 'titles' => [] ]; /** - * @var ServiceFactory - */ - private ServiceFactory $serviceFactory; - - /** + * TaxRate Constructor + * * @param ServiceFactory $serviceFactory + * @param DataMerger $dataMerger */ - public function __construct(ServiceFactory $serviceFactory) - { - $this->serviceFactory = $serviceFactory; + public function __construct( + private readonly ServiceFactory $serviceFactory, + private readonly DataMerger $dataMerger + ) { } /** @@ -46,10 +46,13 @@ public function __construct(ServiceFactory $serviceFactory) public function apply(array $data = []): ?DataObject { $service = $this->serviceFactory->create(TaxRateRepositoryInterface::class, 'save'); + $data = $this->dataMerger->merge( + self::DEFAULT_DATA, + $data + ); + $data['code'] = str_replace('%uniqid%', uniqid(), $data['code']); - return $service->execute([ - 'taxRate' => array_merge(self::DEFAULT_DATA, $data), - ]); + return $service->execute(['taxRate' => $data]); } /** diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index 8ff70de7d6ba..dcf49a1679d3 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -82,6 +82,7 @@ type WishlistCartUserInputError @doc(description: "Contains details about errors enum WishlistCartUserInputErrorType @doc(description: "A list of possible error types.") { PRODUCT_NOT_FOUND + REQUIRED_PARAMETER_MISSING NOT_SALABLE INSUFFICIENT_STOCK UNDEFINED diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogRule/CatalogRuleStoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogRule/CatalogRuleStoreConfigTest.php deleted file mode 100644 index 5d9c0dd427c0..000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogRule/CatalogRuleStoreConfigTest.php +++ /dev/null @@ -1,55 +0,0 @@ -assertEquals( - [ - 'storeConfig' => [ - 'share_all_catalog_rules' => 1, - 'share_applied_catalog_rules' => 1 - ] - ], - $this->graphQlQuery($this->getStoreConfigQuery()) - ); - } - - /** - * Generates storeConfig query with newly added configurations - * - * @return string - */ - private function getStoreConfigQuery(): string - { - return <<fixtures = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage(); - } - - /** - * Test to retrieve all catalog rules when catalog/rule/share_all_catalog_rules is enabled. - * - * @throws Exception - */ - #[ - Config('catalog/rule/share_all_catalog_rules', 1), - DataFixture(CatalogRuleFixture::class, as: 'catalogrule1'), - DataFixture(CatalogRuleFixture::class, as: 'catalogrule2'), - DataFixture(CatalogRuleFixture::class, as: 'catalogrule3'), - DataFixture(CatalogRuleFixture::class, ['is_active' => 0], as: 'catalogrule4') - ] - public function testGetAllCatalogRules(): void - { - $this->assertEmpty( - array_diff( - array_column($this->graphQlQuery($this->getAllCatalogRulesQuery()), 'name'), - array_column($this->fetchAllCatalogRules(), 'name') - ) - ); - } - - /** - * Test to retrieve catalog rules when catalog/rule/share_all_catalog_rules is enabled. - * - * @throws Exception - */ - #[ - Config('catalog/rule/share_all_catalog_rules', 1) - ] - public function testGetAllCatalogRulesWithZeroResult(): void - { - $response = $this->graphQlQuery($this->getAllCatalogRulesQuery()); - $this->assertEmpty($response['allCatalogRules']); - } - - /** - * Test to retrieve all catalog rules when catalog/rule/share_all_catalog_rules is disabled. - * - * @throws Exception - */ - #[ - Config('catalog/rule/share_all_catalog_rules', 0) - ] - public function testGetAllCatalogRulesWhenConfigDisabled(): void - { - $this->expectException(ResponseContainsErrorsException::class); - $this->expectExceptionMessage( - "Sharing catalog rules information is disabled or not configured." - ); - $this->graphQlQuery($this->getAllCatalogRulesQuery()); - } - - /** - * Get all catalog rules - * - * @return array[] - */ - private function fetchAllCatalogRules(): array - { - return [ - 'allCatalogRules' => [ - ['name' => $this->fixtures->get('catalogrule1')->getName()], - ['name' => $this->fixtures->get('catalogrule2')->getName()], - ['name' => $this->fixtures->get('catalogrule3')->getName()] - ] - ]; - } - - /** - * Get all catalog rules query - * - * @return string - */ - private function getAllCatalogRulesQuery(): string - { - return <<fixtures = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage(); - } - - /** - * Test to retrieve applied catalog rules when catalog/rule/share_applied_catalog_rules is enabled. - * - * @throws Exception - */ - #[ - Config('catalog/rule/share_applied_catalog_rules', 1), - DataFixture(ProductFixture::class, as: 'product'), - DataFixture(CatalogRuleFixture::class, as: 'catalogrule1') - ] - public function testGetAppliedCatalogRules(): void - { - $response = $this->graphQlQuery($this->getAppliedCatalogRulesQuery()); - $this->assertContains( - $this->fixtures->get('catalogrule1')->getName(), - array_column($response['products']['items'][0]['rules'], 'name') - ); - } - - /** - * Test to retrieve applied catalog rules when catalog/rule/share_applied_catalog_rules is enabled. - * - * @throws Exception - */ - #[ - Config('catalog/rule/share_applied_catalog_rules', 1), - DataFixture(ProductFixture::class, as: 'product') - ] - public function testGetAppliedCatalogRulesWithZeroResult(): void - { - $response = $this->graphQlQuery($this->getAppliedCatalogRulesQuery()); - $this->assertEmpty($response['products']['items'][0]['rules']); - } - - /** - * Test to retrieve applied catalog rules when catalog/rule/share_applied_catalog_rules is disabled. - * - * @throws Exception - */ - #[ - Config('catalog/rule/share_applied_catalog_rules', 0), - DataFixture(ProductFixture::class, as: 'product') - ] - public function testGetAppliedCatalogRulesWhenConfigDisabled(): void - { - self::assertEquals( - [ - 'products' => [ - 'items' => [ - '0' => [ - 'rules' => null - ] - ] - ] - ], - $this->graphQlQuery($this->getAppliedCatalogRulesQuery()) - ); - } - - /** - * Get applied catalog rules query - * - * @return string - */ - private function getAppliedCatalogRulesQuery(): string - { - return <<fixtures->get('product')->getSku()}" } }){ - items { - rules { - name - } - } - } - } - QUERY; - } -} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CustomerStoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CustomerStoreConfigTest.php index 4f6afcc90f25..cc6c98df5084 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CustomerStoreConfigTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CustomerStoreConfigTest.php @@ -7,7 +7,6 @@ namespace Magento\GraphQl\Customer; -use Exception; use Magento\TestFramework\Fixture\Config; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -16,20 +15,30 @@ */ class CustomerStoreConfigTest extends GraphQlAbstract { - /** - * @throws Exception - */ #[ - Config('customer/account_information/graphql_share_all_customer_groups', 1), - Config('customer/account_information/graphql_share_customer_group', 1) + Config('customer/account_information/graphql_share_customer_group', true) ] public function testCustomerGroupsGraphQlStoreConfig(): void { $this->assertEquals( [ 'storeConfig' => [ - 'graphql_share_all_customer_groups' => 1, - 'graphql_share_customer_group' => 1 + 'graphql_share_customer_group' => true + ] + ], + $this->graphQlQuery($this->getStoreConfigQuery()) + ); + } + + #[ + Config('customer/account_information/graphql_share_customer_group', false) + ] + public function testCustomerGroupsGraphQlStoreConfigDisabled(): void + { + $this->assertEquals( + [ + 'storeConfig' => [ + 'graphql_share_customer_group' => false ] ], $this->graphQlQuery($this->getStoreConfigQuery()) @@ -46,7 +55,6 @@ private function getStoreConfigQuery(): string return << $this->fetchAllCustomerGroups() - ], - $this->graphQlQuery($this->getAllCustomerGroupsQuery()) - ); - } - - /** - * Test to retrieve all customer groups when graphql_share_all_customer_groups is disabled. - * - * @throws Exception - */ - #[ - Config('customer/account_information/graphql_share_all_customer_groups', 0) - ] - public function testGetAllCustomerGroupsWhenConfigDisabled(): void - { - $this->expectException(ResponseContainsErrorsException::class); - $this->expectExceptionMessage( - "Sharing customer group information is disabled or not configured." - ); - $this->graphQlQuery($this->getAllCustomerGroupsQuery()); - } - - /** - * Fetch all customer groups - * - * @return array|array[] - * @throws LocalizedException - */ - public function fetchAllCustomerGroups(): array - { - $groupRepository = Bootstrap::getObjectManager()->get(GroupRepositoryInterface::class); - $searchCriteria = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class)->create(); - - $customerGroups = $groupRepository->getList($searchCriteria)->getItems(); - - return array_map( - static fn ($group) => ['name' => $group->getCode()], - $customerGroups - ); - } - - /** - * Get all customer groups query - * - * @return string - */ - private function getAllCustomerGroupsQuery(): string - { - return <<customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); $this->fixtures = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage(); - $this->groupRepository = Bootstrap::getObjectManager()->get(GroupRepositoryInterface::class); + $this->idEncoder = Bootstrap::getObjectManager()->get(Uid::class); } /** * Test to retrieve customer group when graphql_share_customer_group is enabled. @@ -61,7 +51,7 @@ protected function setUp(): void * @throws Exception */ #[ - Config('customer/account_information/graphql_share_customer_group', 1), + Config('customer/account_information/graphql_share_customer_group', true), DataFixture(CustomerFixture::class, as: 'customer') ] public function testGetCustomerGroupForLoggedInCustomer(): void @@ -72,7 +62,7 @@ public function testGetCustomerGroupForLoggedInCustomer(): void [ 'customer' => [ 'group' => [ - 'name' => $this->groupRepository->getById($customer->getGroupId())->getCode() + 'uid' => $this->idEncoder->encode($customer->getGroupId()) ] ] ], @@ -86,12 +76,12 @@ public function testGetCustomerGroupForLoggedInCustomer(): void } /** - * Test to retrieve customer group when graphql_share_customer_group is disabled. + * Test to retrieve customer group when graphql_share_customer_group is disabled. * * @throws Exception */ #[ - Config('customer/account_information/graphql_share_customer_group', 0), + Config('customer/account_information/graphql_share_customer_group', false), DataFixture(CustomerFixture::class, as: 'customer') ] public function testGetCustomerGroupForLoggedInCustomerWhenConfigDisabled(): void @@ -111,11 +101,8 @@ public function testGetCustomerGroupForLoggedInCustomerWhenConfigDisabled(): voi ); } - /** - * @throws Exception - */ #[ - Config('customer/account_information/graphql_share_customer_group', 1), + Config('customer/account_information/graphql_share_customer_group', true), DataFixture(CustomerFixture::class, as: 'customer') ] public function testGetCustomerGroup(): void @@ -125,7 +112,7 @@ public function testGetCustomerGroup(): void self::assertEquals( [ 'customerGroup' => [ - 'name' => $this->groupRepository->getById($customer->getGroupId())->getCode() + 'uid' => $this->idEncoder->encode($customer->getGroupId()) ] ], $this->graphQlQuery( @@ -141,53 +128,7 @@ public function testGetCustomerGroup(): void * @throws Exception */ #[ - Config('customer/account_information/graphql_share_customer_group', 1), - DataFixture(CustomerGroupFixture::class, as: 'group') - ] - public function testGetCustomerGroupWhenItsExcluded(): void - { - $customer = Bootstrap::getObjectManager()->get(Customer::class); - $customerGroup = Bootstrap::getObjectManager()->get(Group::class); - - // Load Customer Group - $customerGroup->load($this->fixtures->get('group')->getCode(), 'customer_group_code'); - - // Ensure extension attributes exist - $extensionAttributes = $customerGroup->getExtensionAttributes(); - if (!$extensionAttributes) { - $extensionAttributes = Bootstrap::getObjectManager()->get(GroupExtension::class); - $customerGroup->setExtensionAttributes($extensionAttributes); - } - - // Set excluded website ID - $extensionAttributes->setExcludeWebsiteIds([1]); // Website ID 1 is excluded - $customerGroup->setExtensionAttributes($extensionAttributes); - $customerGroup->save(); - - //set customer - $customer->setWebsiteId(1); - $customer->setGroupId($customerGroup->getId()); - $customer->setEmail('excluded_customer@example.com'); - $customer->setFirstname('Excluded'); - $customer->setLastname('User'); - $customer->setPassword('password'); - $customer->save(); - - $response = $this->graphQlQuery( - $this->getCustomerGroupQuery(), - [], - '', - $this->getCustomerAuthHeaders('excluded_customer@example.com') - ); - - self::assertNotEquals($customer->getCustomerGroup(), $response['customerGroup']['name']); - } - - /** - * @throws Exception - */ - #[ - Config('customer/account_information/graphql_share_customer_group', 0), + Config('customer/account_information/graphql_share_customer_group', false), DataFixture(CustomerFixture::class, as: 'customer') ] public function testGetCustomerGroupWhenConfigDisabled(): void @@ -208,14 +149,14 @@ public function testGetCustomerGroupWhenConfigDisabled(): void * @throws Exception */ #[ - Config('customer/account_information/graphql_share_customer_group', 1) + Config('customer/account_information/graphql_share_customer_group', true) ] public function testGetCustomerGroupForGuest(): void { self::assertEquals( [ 'customerGroup' => [ - 'name' => self::GUEST_CUSTOMER_GROUP + 'uid' => $this->idEncoder->encode('0') ] ], $this->graphQlQuery($this->getCustomerGroupQuery()) @@ -226,7 +167,7 @@ public function testGetCustomerGroupForGuest(): void * @throws Exception */ #[ - Config('customer/account_information/graphql_share_customer_group', 0) + Config('customer/account_information/graphql_share_customer_group', false) ] public function testGetCustomerGroupForGuestWhenConfigDisabled(): void { @@ -247,7 +188,7 @@ private function getCustomerGroupQuery(): string return <<fixtures = DataFixtureStorageManager::getStorage(); + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + } + + #[ + DataFixture(AttributeFixture::class, [ + 'frontend_input' => 'select', + 'options' => ['40', '42'], + 'is_configurable' => true, + 'is_global' => true + ], as: 'attribute'), + DataFixture( + ProductFixture::class, + [ + 'price' => 100, + 'custom_attributes' => [ + ['attribute_code' => '$attribute.attribute_code$', 'value' => '40'] + ] + ], + as: 'product1' + ), + DataFixture( + ProductFixture::class, + [ + 'price' => 100, + 'custom_attributes' => [ + ['attribute_code' => '$attribute.attribute_code$', 'value' => '42'] + ] + ], + as: 'product2' + ), + DataFixture( + ConfigurableProductFixture::class, + [ + '_options' => ['$attribute$'], + '_links' => ['$product1$', '$product2$'], + 'custom_attributes' => [ + ['attribute_code' => '$attribute.attribute_code$', 'value' => '40'] + ] + ], + 'configurable_product' + ), + DataFixture(Customer::class, as: 'customer'), + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], as: 'customerCart'), + DataFixture(QuoteIdMask::class, ['cart_id' => '$customerCart.id$'], 'quoteIdMask'), + ] + public function testAddToCartForConfigurableProductWithoutOptions(): void + { + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + + $this->assertEquals( + [ + 'addProductsToCart' => [ + 'cart' => [ + 'id' => $maskedQuoteId, + 'itemsV2' => [ + 'items' => [] + ] + ], + 'user_errors' => [ + [ + 'code' => 'REQUIRED_PARAMETER_MISSING', + 'message' => 'You need to choose options for your item.' + ] + ] + ] + ], + $this->graphQlMutation( + $this->getAddToCartMutation( + $maskedQuoteId, + $this->fixtures->get('configurable_product')->getSku(), + 2 + ), + [], + "", + $this->getCustomerAuthHeaders($this->fixtures->get('customer')->getEmail()) + ) + ); + } + + /** + * Get addToCart mutation for a configurable product without specifying options + * + * @param string $cartId + * @param string $sku + * @param int $quantity + * @return string + */ + private function getAddToCartMutation(string $cartId, string $sku, int $quantity): string + { + return <<customerTokenService->createCustomerAccessToken($email, 'password'); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartItemAvailabilityTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartItemAvailabilityTest.php new file mode 100644 index 000000000000..16f841b53106 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartItemAvailabilityTest.php @@ -0,0 +1,119 @@ +fixtures = DataFixtureStorageManager::getStorage(); + } + + #[ + Config('cataloginventory/options/not_available_message', 1), + DbIsolation(false), + AppIsolation(true), + DataFixture(SourceFixture::class, as: 'source2'), + DataFixture(StockFixture::class, as: 'stock2'), + DataFixture( + StockSourceLinksFixture::class, + [ + ['stock_id' => '$stock2.stock_id$', 'source_code' => '$source2.source_code$'], + ] + ), + DataFixture( + StockSalesChannelsFixture::class, + ['stock_id' => '$stock2.stock_id$', 'sales_channels' => ['base']] + ), + + DataFixture(ProductFixture::class, ['sku' => 'simple1'], 'p1'), + DataFixture( + SourceItemsFixture::class, + [ + ['sku' => '$p1.sku$', 'source_code' => 'default', 'quantity' => 0], + ['sku' => '$p1.sku$', 'source_code' => '$source2.source_code$', 'quantity' => 100], + ] + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$p1.id$']), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') + ] + public function testCartItemAvailabilityWithMSI(): void + { + $this->assertEquals( + [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + [ + 'not_available_message' => null, + 'is_available' => true, + 'product' => [ + 'quantity' => 100, + ], + ], + ], + ], + ], + ], + $this->graphQlQuery($this->getCartQuery( + $this->fixtures->get('quoteIdMask')->getMaskedId() + )) + ); + } + + /** + * Return cart query with is_available & not_available_message fields + * + * @param string $cartId + * @return string + */ + private function getCartQuery(string $cartId): string + { + return <<fixtures = DataFixtureStorageManager::getStorage(); + } + + #[ + ConfigFixture('cataloginventory/options/enable_inventory_check', false, "store", "default"), + ConfigFixture('cataloginventory/options/not_available_message', true, "store", "default"), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(GuestCart::class, as: 'cart'), + DataFixture(QuoteIdMask::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + DataFixture( + AddProductToCartFixture::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$product.id$', + 'qty' => 1 + ] + ) + ] + public function testNotAvailableMessageWithoutInventoryCheck(): void + { + $this->assertCartResponse(); + } + + #[ + ConfigFixture('cataloginventory/options/enable_inventory_check', true, "store", "default"), + ConfigFixture('cataloginventory/options/not_available_message', true, "store", "default"), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(GuestCart::class, as: 'cart'), + DataFixture(QuoteIdMask::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + DataFixture( + AddProductToCartFixture::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$product.id$', + 'qty' => 1 + ] + ) + ] + public function testNotAvailableMessageWithInventoryCheck(): void + { + $this->assertCartResponse(); + } + + private function assertCartResponse(): void + { + $this->assertEquals( + [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + [ + 'not_available_message' => null, + ] + ] + ] + ] + ], + $this->graphQlQuery( + $this->getCartQuery($this->fixtures->get('quoteIdMask')->getMaskedId()) + ) + ); + } + + /** + * Get cart query with not available message + * + * @param string $maskedQuoteId + * @return string + */ + private function getCartQuery(string $maskedQuoteId): string + { + return <<fixtures = DataFixtureStorageManager::getStorage(); $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $this->stockRegistry = Bootstrap::getObjectManager()->get(StockRegistryInterface::class); } #[ Config('cataloginventory/options/not_available_message', 0), - DataFixture(ProductFixture::class, ['price' => 100.00], as: 'product'), + DataFixture(ProductFixture::class, as: 'product'), DataFixture(GuestCartFixture::class, as: 'cart'), DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 100]), - DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), - DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 90], 'prodStock') + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') ] public function testStockStatusUnavailableSimpleProduct(): void { - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->getQuery($maskedQuoteId); - $response = $this->graphQlQuery($query); - $responseDataObject = new DataObject($response); + $this->updateProductStock(); - self::assertFalse( - $responseDataObject->getData('cart/itemsV2/items/0/is_available') - ); - self::assertEquals( - 'Not enough items for sale', - $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') + $this->assertEquals( + [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + [ + 'is_available' => false, + 'not_available_message' => 'Not enough items for sale', + 'product' => [ + 'sku' => $this->fixtures->get('product')->getSku(), + 'only_x_left_in_stock' => null, + ], + 'quantity' => 100, + ] + ] + ] + ] + ], + $this->graphQlQuery( + $this->getCartQuery($this->fixtures->get('quoteIdMask')->getMaskedId()) + ) ); } @@ -94,65 +108,100 @@ public function testStockStatusUnavailableSimpleProduct(): void ] public function testStockStatusAvailableSimpleProduct(): void { - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->getQuery($maskedQuoteId); - $response = $this->graphQlQuery($query); - $responseDataObject = new DataObject($response); - - self::assertTrue( - $responseDataObject->getData('cart/itemsV2/items/0/is_available') - ); - self::assertNull( - $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') + $this->assertEquals( + [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + [ + 'is_available' => true, + 'not_available_message' => null, + 'product' => [ + 'sku' => $this->fixtures->get('product')->getSku(), + 'only_x_left_in_stock' => 100, + ], + 'quantity' => 100 + ] + ] + ] + ] + ], + $this->graphQlQuery( + $this->getCartQuery($this->fixtures->get('quoteIdMask')->getMaskedId()) + ) ); } #[ Config('cataloginventory/options/not_available_message', 1), Config('cataloginventory/options/stock_threshold_qty', 100), - DataFixture(ProductFixture::class, ['price' => 100.00], as: 'product'), + DataFixture(ProductFixture::class, as: 'product'), DataFixture(GuestCartFixture::class, as: 'cart'), - DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 20]), - DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), - DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 10], 'prodStock') + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') ] public function testStockStatusUnavailableSimpleProductOption1(): void { - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->getQuery($maskedQuoteId); - $response = $this->graphQlQuery($query); - $responseDataObject = new DataObject($response); + $this->updateProductStock(10, true); - self::assertFalse( - $responseDataObject->getData('cart/itemsV2/items/0/is_available') - ); - self::assertEquals(10, $responseDataObject->getData('cart/itemsV2/items/0/product/only_x_left_in_stock')); - self::assertEquals( - 'Only 10 of 20 available', - $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') + $this->assertEquals( + [ + 'addProductsToCart' => [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [], + ], + ], + 'user_errors' => [ + [ + 'code' => 'INSUFFICIENT_STOCK', + 'message' => 'Only 10 of 20 available', + ] + ] + ] + ], + $this->graphQlMutation( + $this->addToCartMutation( + $this->fixtures->get('quoteIdMask')->getMaskedId(), + $this->fixtures->get('product')->getSku(), + 20 + ) + ) ); } #[ Config('cataloginventory/options/not_available_message', 1), Config('cataloginventory/options/stock_threshold_qty', 100), - DataFixture(ProductFixture::class, ['sku' => self::SKU, 'price' => 100.00], as: 'product'), + DataFixture(ProductFixture::class, as: 'product'), DataFixture(GuestCartFixture::class, as: 'cart'), DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 99]), DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') ] public function testStockStatusAddSimpleProduct(): void { - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->mutationAddSimpleProduct($maskedQuoteId, self::SKU, 1); - $response = $this->graphQlMutation($query); - $responseDataObject = new DataObject($response); - - self::assertTrue( - $responseDataObject->getData('addProductsToCart/cart/itemsV2/items/0/is_available') - ); - self::assertNull( - $responseDataObject->getData('addProductsToCart/cart/itemsV2/items/0/not_available_message') + $this->assertEquals( + [ + 'addProductsToCart' => [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + [ + 'not_available_message' => null, + 'quantity' => 100, + 'is_available' => true, + ] + ] + ] + ], + 'user_errors' => [], + ], + ], + $this->graphQlMutation( + $this->addToCartMutation( + $this->fixtures->get('quoteIdMask')->getMaskedId(), + $this->fixtures->get('product')->getSku() + ) + ) ); } @@ -183,79 +232,40 @@ public function testStockStatusAddSimpleProduct(): void 'qty' => 100 ], ), - DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), - DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 90], 'prodStock') + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') ] public function testStockStatusUnavailableBundleProduct(): void { - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->getQuery($maskedQuoteId); - $response = $this->graphQlQuery($query); - $responseDataObject = new DataObject($response); + $this->updateProductStock(); - self::assertFalse( - $responseDataObject->getData('cart/itemsV2/items/0/is_available') - ); - self::assertEquals( - 'Not enough items for sale', - $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') - ); - } - - #[ - Config('cataloginventory/options/not_available_message', 1), - Config('cataloginventory/options/stock_threshold_qty', 100), - DataFixture(ProductFixture::class, ['price' => 100.00], as: 'product'), - DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 100], 'prodStock'), - DataFixture( - BundleSelectionFixture::class, + $this->assertEquals( [ - 'sku' => '$product.sku$', 'price' => 100, 'price_type' => 0 + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + [ + 'is_available' => null, + 'not_available_message' => 'Not enough items for sale', + 'quantity' => 100, + 'product' => [ + 'sku' => $this->fixtures->get('bundleProduct')->getSku(), + 'only_x_left_in_stock' => null, + ] + ] + ] + ] + ] ], - as:'link' - ), - DataFixture(BundleOptionFixture::class, ['title' => 'Checkbox Options', 'type' => 'checkbox', - 'required' => 1,'product_links' => ['$link$']], 'option'), - DataFixture( - BundleProductFixture::class, - ['price' => 90, '_options' => ['$option$']], - as:'bundleProduct' - ), - DataFixture(ProductStockFixture::class, ['prod_id' => '$bundleProduct.id$', 'prod_qty' => 100], 'prodStock'), - DataFixture(GuestCartFixture::class, as: 'cart'), - DataFixture( - AddBundleProductToCart::class, - [ - 'cart_id' => '$cart.id$', - 'product_id' => '$bundleProduct.id$', - 'selections' => [['$product.id$']], - 'qty' => 100 - ], - ), - DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), - DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 90], 'prodStock') - ] - public function testStockStatusUnavailableBundleProductOption1(): void - { - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->getQuery($maskedQuoteId); - $response = $this->graphQlQuery($query); - $responseDataObject = new DataObject($response); - - self::assertFalse( - $responseDataObject->getData('cart/itemsV2/items/0/is_available') - ); - self::assertEquals( - 'Only 90 of 100 available', - $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') + $this->graphQlQuery( + $this->getCartQuery($this->fixtures->get('quoteIdMask')->getMaskedId()) + ) ); } #[ Config('cataloginventory/options/not_available_message', 1), Config('cataloginventory/options/stock_threshold_qty', 100), - DataFixture(ProductFixture::class, ['price' => 100.00], as: 'product'), - DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 100], 'prodStock'), + DataFixture(ProductFixture::class, as: 'product'), DataFixture( BundleSelectionFixture::class, [ @@ -307,21 +317,38 @@ public function testStockStatusAddBundleProduct(): void $option = $typeInstance->getOptionsCollection($product)->getFirstItem(); /** @var \Magento\Catalog\Model\Product $selection */ $selection = $typeInstance->getSelectionsCollection([$option->getId()], $product)->getFirstItem(); - $optionId = $option->getId(); - $selectionId = $selection->getSelectionId(); - - $bundleOptionIdV2 = $this->generateBundleOptionIdV2((int) $optionId, (int) $selectionId, 1); - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->mutationAddBundleProduct($maskedQuoteId, self::PARENT_SKU_BUNDLE, $bundleOptionIdV2); - $response = $this->graphQlMutation($query); - $responseDataObject = new DataObject($response); - - self::assertTrue( - $responseDataObject->getData('addProductsToCart/cart/itemsV2/items/0/is_available') + $bundleOptionIdV2 = $this->generateBundleOptionIdV2( + (int) $option->getId(), + (int) $selection->getSelectionId(), + 1 ); - self::assertNull( - $responseDataObject->getData('addProductsToCart/cart/itemsV2/items/0/not_available_message') + + $this->assertEquals( + [ + 'addProductsToCart' => [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + [ + 'is_available' => true, + 'not_available_message' => null, + 'product' => [ + 'sku' => self::PARENT_SKU_BUNDLE, + ] + ] + ] + ] + ] + ] + ], + $this->graphQlMutation( + $this->mutationAddBundleProduct( + $this->fixtures->get('quoteIdMask')->getMaskedId(), + self::PARENT_SKU_BUNDLE, + $bundleOptionIdV2 + ) + ) ); } @@ -344,64 +371,32 @@ public function testStockStatusAddBundleProduct(): void 'child_product_id' => '$product.id$', 'qty' => 100 ], - ), - DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 90], 'prodStock') + ) ] public function testStockStatusUnavailableConfigurableProduct(): void { - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->getQuery($maskedQuoteId); - $response = $this->graphQlQuery($query); - $responseDataObject = new DataObject($response); - - self::assertFalse( - $responseDataObject->getData('cart/itemsV2/items/0/is_available') - ); - self::assertEquals( - 'Not enough items for sale', - $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') - ); - } - - #[ - Config('cataloginventory/options/not_available_message', 1), - Config('cataloginventory/options/stock_threshold_qty', 100), - DataFixture(ProductFixture::class, as: 'product'), - DataFixture(AttributeFixture::class, as: 'attribute'), - DataFixture( - ConfigurableProductFixture::class, - ['_options' => ['$attribute$'], '_links' => ['$product$']], - 'configurable_product' - ), - DataFixture(GuestCartFixture::class, as: 'cart'), - DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), - DataFixture( - AddConfigurableProductToCartFixture::class, + $this->updateProductStock(); + $this->assertEquals( [ - 'cart_id' => '$cart.id$', - 'product_id' => '$configurable_product.id$', - 'child_product_id' => '$product.id$', - 'qty' => 100 + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + [ + 'is_available' => false, + 'not_available_message' => 'Not enough items for sale', + 'quantity' => 100, + 'product' => [ + 'sku' => $this->fixtures->get('configurable_product')->getSku(), + 'only_x_left_in_stock' => null, + ] + ] + ] + ] + ] ], - ), - DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 90], 'prodStock') - ] - public function testStockStatusUnavailableConfigurableProductOption1(): void - { - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->getQuery($maskedQuoteId); - $response = $this->graphQlQuery($query); - $responseDataObject = new DataObject($response); - - self::assertFalse( - $responseDataObject->getData('cart/itemsV2/items/0/is_available') - ); - - self::assertEquals(90, $responseDataObject->getData('cart/itemsV2/items/0/product/only_x_left_in_stock')); - - self::assertEquals( - 'Only 90 of 100 available', - $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') + $this->graphQlQuery( + $this->getCartQuery($this->fixtures->get('quoteIdMask')->getMaskedId()) + ) ); } @@ -417,7 +412,6 @@ public function testStockStatusUnavailableConfigurableProductOption1(): void ), DataFixture(GuestCartFixture::class, as: 'cart'), DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), - DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 100], 'prodStock'), DataFixture( AddConfigurableProductToCartFixture::class, [ @@ -430,17 +424,27 @@ public function testStockStatusUnavailableConfigurableProductOption1(): void ] public function testStockStatusAvailableConfigurableProduct(): void { - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->getQuery($maskedQuoteId); - $response = $this->graphQlQuery($query); - $responseDataObject = new DataObject($response); - - self::assertTrue( - $responseDataObject->getData('cart/itemsV2/items/0/is_available') - ); - - self::assertNull( - $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') + $this->assertEquals( + [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + [ + 'is_available' => true, + 'not_available_message' => null, + 'quantity' => 90, + 'product' => [ + 'sku' => $this->fixtures->get('configurable_product')->getSku(), + 'only_x_left_in_stock' => 100, + ] + ] + ] + ] + ] + ], + $this->graphQlQuery( + $this->getCartQuery($this->fixtures->get('quoteIdMask')->getMaskedId()) + ) ); } @@ -480,7 +484,7 @@ public function testStockStatusAvailableConfigurableProduct(): void DataFixture( ProductStockFixture::class, [ - 'prod_id' => 'product_variant_1.id$', + 'prod_id' => '$product_variant_1.id$', 'prod_qty' => 100 ], 'productVariantStock1' @@ -488,7 +492,7 @@ public function testStockStatusAvailableConfigurableProduct(): void DataFixture( ProductStockFixture::class, [ - 'prod_id' => 'product_variant_2.id$', + 'prod_id' => '$product_variant_2.id$', 'prod_qty' => 100 ], 'productVariantStock2' @@ -499,191 +503,270 @@ public function testStockStatusAvailableConfigurableProduct(): void ] public function testStockStatusAddConfigurableProduct(): void { - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); $productVariant1 = $this->fixtures->get('product_variant_1'); /** @var AttributeInterface $attribute */ $attribute = $this->fixtures->get('attribute'); /** @var AttributeOptionInterface $option */ $option = $attribute->getOptions()[1]; $selectedOption = base64_encode("configurable/{$attribute->getAttributeId()}/{$option->getValue()}"); - $query = $this->mutationAddConfigurableProduct( - $maskedQuoteId, - $productVariant1->getData('sku'), - $selectedOption, - 100 - ); - - $response = $this->graphQlMutation($query); - $responseDataObject = new DataObject($response); - self::assertTrue( - $responseDataObject->getData('addProductsToCart/cart/itemsV2/items/0/is_available') - ); - - self::assertNull( - $responseDataObject->getData('addProductsToCart/cart/itemsV2/items/0/not_available_message') + $this->assertEquals( + [ + 'addProductsToCart' => [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + [ + 'quantity' => 100, + 'is_available' => 1, + 'not_available_message' => '', + 'product' => [ + 'sku' => 'product_variant_1', + 'only_x_left_in_stock' => 100, + ] + ] + ] + ] + ], + 'user_errors' => [], + ], + ], + $this->graphQlMutation( + $this->mutationAddConfigurableProduct( + $this->fixtures->get('quoteIdMask')->getMaskedId(), + $productVariant1->getData('sku'), + $selectedOption, + 100 + ) + ) ); } #[ Config('cataloginventory/options/not_available_message', 1), Config('cataloginventory/options/stock_threshold_qty', 100), - DataFixture(ProductFixture::class, ['price' => 100.00], as: 'product'), + DataFixture(ProductFixture::class, as: 'product'), DataFixture(GuestCartFixture::class, as: 'cart'), - DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 100]), - DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), - DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 90], 'prodStock') + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') ] public function testNotAvailableMessageOption1(): void { - $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->getQuery($maskedQuoteId); - $response = $this->graphQlQuery($query); - $responseDataObject = new DataObject($response); - - self::assertFalse( - $responseDataObject->getData('cart/itemsV2/items/0/is_available') - ); - - self::assertEquals(90, $responseDataObject->getData('cart/itemsV2/items/0/product/only_x_left_in_stock')); - - self::assertEquals( - 'Only 90 of 100 available', - $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') + $this->updateProductStock(90, true); + $this->assertEquals( + [ + 'addProductsToCart' => [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [], + ], + ], + 'user_errors' => [ + [ + 'code' => 'INSUFFICIENT_STOCK', + 'message' => 'Only 90 of 100 available', + ] + ] + ] + ], + $this->graphQlMutation( + $this->addToCartMutation( + $this->fixtures->get('quoteIdMask')->getMaskedId(), + $this->fixtures->get('product')->getSku(), + 100 + ) + ) ); } /** + * Generate GraphQL query to get cart items with availability status + * * @param string $cartId * @return string */ - private function getQuery(string $cartId): string + private function getCartQuery(string $cartId): string { return <<fixtures->get('product'); + $stockItem = $this->stockRegistry->getStockItem($product->getId()); + $stockItem->setData(StockItemInterface::IS_IN_STOCK, $isInStock); + $stockItem->setData(StockItemInterface::QTY, $qty); + $stockItem->setData(StockItemInterface::MANAGE_STOCK, true); + $stockItem->save(); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CustomerOrderItemProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CustomerOrderItemProductTest.php new file mode 100644 index 000000000000..5d5ea35f51de --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CustomerOrderItemProductTest.php @@ -0,0 +1,155 @@ +customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->fixtures = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage(); + $this->stockRegistry = Bootstrap::getObjectManager()->get(StockRegistryInterface::class); + } + + #[ + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(CustomerFixture::class, as: 'customer'), + DataFixture( + CustomerCartFixture::class, + ['customer_id' => '$customer.id$', 'reserved_order_id' => 'test_order_with_simple_product'], + as: 'cart' + ), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']), + DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(PlaceOrderFixture::class, ['cart_id' => '$cart.id$'], 'order') + ] + public function testOrderItemProductWhenOutOfStock(): void + { + $this->updateProductStock(); + + $this->assertEquals( + [ + 'customer' => [ + 'orders' => [ + 'items' => [ + [ + 'number' => $this->fixtures->get('order')->getIncrementId(), + 'items' => [ + [ + 'product' => [ + 'sku' => $this->fixtures->get('product')->getSku(), + 'stock_status' => 'OUT_OF_STOCK' + ] + ] + ] + ] + ] + ] + ] + ], + $this->graphQlQuery( + $this->getCustomerOrdersQuery(), + [], + '', + $this->getCustomerAuthHeaders($this->fixtures->get('customer')->getEmail()) + ) + ); + } + + /** + * Update product stock to out of stock + * + * @throws Exception + */ + private function updateProductStock(): void + { + /** @var ProductInterface $product */ + $product = $this->fixtures->get('product'); + $stockItem = $this->stockRegistry->getStockItem($product->getId()); + $stockItem->setData(StockItemInterface::IS_IN_STOCK, false); + $stockItem->setData(StockItemInterface::QTY, 0); + $stockItem->setData(StockItemInterface::MANAGE_STOCK, true); + $stockItem->save(); + } + + /** + * Returns the GraphQL query to fetch customer orders. + * + * @return string + */ + private function getCustomerOrdersQuery(): string + { + return <<customerTokenService->createCustomerAccessToken($email, 'password'); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrderTotalGrandTotalExclTaxTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrderTotalGrandTotalExclTaxTest.php new file mode 100644 index 000000000000..9f78a09a7403 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrderTotalGrandTotalExclTaxTest.php @@ -0,0 +1,332 @@ +customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->fixtures = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage(); + } + + #[ + Config('tax/calculation/apply_after_discount', false, "store", "default"), + DataFixture( + AddressConditionFixture::class, + [ + 'attribute' => 'total_qty', + 'operator' => '>=', + 'value' => 1 + ], + 'condition' + ), + DataFixture( + SalesRuleFixture::class, + [ + 'store_labels' => [1 => self::DISCOUNT_LABEL], + 'coupon_type' => SalesRule::COUPON_TYPE_SPECIFIC, + 'simple_action' => SalesRule::BY_PERCENT_ACTION, + 'discount_amount' => 100, + 'coupon_code' => self::COUPON_CODE, + 'conditions' => ['$condition$'], + 'uses_per_customer' => 10, + 'apply_to_shipping' => true, + 'stop_rules_processing' => true + ], + as: 'rule' + ), + DataFixture(ProductTaxClassFixture::class, as: 'product_tax_class'), + DataFixture(TaxRateFixture::class, as: 'rate'), + DataFixture( + TaxRuleFixture::class, + [ + 'customer_tax_class_ids' => [3], + 'product_tax_class_ids' => ['$product_tax_class.classId$'], + 'tax_rate_ids' => ['$rate.id$'] + ], + 'rule' + ), + DataFixture(ProductFixture::class, [ + 'price' => self::PRODUCT_PRICE, + 'custom_attributes' => ['tax_class_id' => '$product_tax_class.classId$'] + ], as: 'product'), + DataFixture(CustomerFixture::class, as: 'customer'), + DataFixture(CustomerCartFixture::class, ['customer_id' => '$customer.id$'], as: 'quote'), + DataFixture( + AddProductToCartFixture::class, + [ + 'cart_id' => '$quote.id$', + 'product_id' => '$product.id$', + 'qty' => self::TOTAL_QTY + ] + ), + DataFixture( + ApplyCouponFixture::class, + [ + 'cart_id' => '$quote.id$', + 'coupon_codes' => [self::COUPON_CODE] + ] + ), + DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(PlaceOrderFixture::class, ['cart_id' => '$quote.id$'], 'order') + ] + public function testGrandTotalExclTaxWithTaxAppliedBeforeDiscount(): void + { + $this->assertOrderResponse(); + } + + #[ + Config('tax/calculation/apply_after_discount', true, "store", "default"), + DataFixture( + AddressConditionFixture::class, + [ + 'attribute' => 'total_qty', + 'operator' => '>=', + 'value' => 1 + ], + 'condition' + ), + DataFixture( + SalesRuleFixture::class, + [ + 'store_labels' => [1 => self::DISCOUNT_LABEL], + 'coupon_type' => SalesRule::COUPON_TYPE_SPECIFIC, + 'simple_action' => SalesRule::BY_PERCENT_ACTION, + 'discount_amount' => 100, + 'coupon_code' => self::COUPON_CODE, + 'conditions' => ['$condition$'], + 'uses_per_customer' => 10, + 'apply_to_shipping' => true, + 'stop_rules_processing' => true + ], + as: 'rule' + ), + DataFixture(ProductTaxClassFixture::class, as: 'product_tax_class'), + DataFixture(TaxRateFixture::class, as: 'rate'), + DataFixture( + TaxRuleFixture::class, + [ + 'customer_tax_class_ids' => [3], + 'product_tax_class_ids' => ['$product_tax_class.classId$'], + 'tax_rate_ids' => ['$rate.id$'] + ], + 'rule' + ), + DataFixture(ProductFixture::class, [ + 'price' => self::PRODUCT_PRICE, + 'custom_attributes' => ['tax_class_id' => '$product_tax_class.classId$'] + ], as: 'product'), + DataFixture(CustomerFixture::class, as: 'customer'), + DataFixture(CustomerCartFixture::class, ['customer_id' => '$customer.id$'], as: 'quote'), + DataFixture( + AddProductToCartFixture::class, + [ + 'cart_id' => '$quote.id$', + 'product_id' => '$product.id$', + 'qty' => self::TOTAL_QTY + ] + ), + DataFixture( + ApplyCouponFixture::class, + [ + 'cart_id' => '$quote.id$', + 'coupon_codes' => [self::COUPON_CODE] + ] + ), + DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(PlaceOrderFixture::class, ['cart_id' => '$quote.id$'], 'order') + ] + public function testGrandTotalExclTaxWithTaxAppliedAfterDiscount(): void + { + $this->assertOrderResponse(); + } + + #[ + Config('tax/calculation/apply_after_discount', false, "store", "default"), + DataFixture(ProductTaxClassFixture::class, as: 'product_tax_class'), + DataFixture(TaxRateFixture::class, as: 'rate'), + DataFixture( + TaxRuleFixture::class, + [ + 'customer_tax_class_ids' => [3], + 'product_tax_class_ids' => ['$product_tax_class.classId$'], + 'tax_rate_ids' => ['$rate.id$'] + ], + 'rule' + ), + DataFixture(ProductFixture::class, [ + 'price' => self::PRODUCT_PRICE, + 'custom_attributes' => ['tax_class_id' => '$product_tax_class.classId$'] + ], as: 'product'), + DataFixture(CustomerFixture::class, as: 'customer'), + DataFixture(CustomerCartFixture::class, ['customer_id' => '$customer.id$'], as: 'quote'), + DataFixture( + AddProductToCartFixture::class, + [ + 'cart_id' => '$quote.id$', + 'product_id' => '$product.id$', + 'qty' => self::TOTAL_QTY + ] + ), + DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(PlaceOrderFixture::class, ['cart_id' => '$quote.id$'], 'order') + ] + public function testGrandTotalExclTaxWithoutDiscount(): void + { + $this->assertOrderResponse(); + } + + /** + * Assert order response for grand total excluding tax + * + * @return void + * @throws AuthenticationException|LocalizedException + * @throws Exception + */ + private function assertOrderResponse(): void + { + /** @var OrderInterface $order */ + $order = $this->fixtures->get('order'); + $this->assertEquals( + [ + 'customer' => [ + 'orders' => [ + 'items' => [ + [ + 'number' => $order->getIncrementId(), + 'total' => [ + 'grand_total' => [ + 'value' => $order->getGrandTotal(), + 'currency' => 'USD' + ], + 'grand_total_excl_tax' => [ + 'value' => (float)($order->getSubtotal() + + $order->getShippingAmount() + - abs((float)$order->getDiscountAmount())), + 'currency' => 'USD' + ] + ] + ] + ] + ] + ] + ], + $this->graphQlQuery( + $this->getCustomerOrdersQuery( + $order->getIncrementId() + ), + [], + '', + $this->getCustomerAuthHeaders($this->fixtures->get('customer')->getEmail()) + ) + ); + } + + /** + * Get customer orders query with total fields + * + * @param string $orderId + * @return string + */ + private function getCustomerOrdersQuery(string $orderId): string + { + return <<customerTokenService->createCustomerAccessToken($email, 'password'); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/SalesRule/CartRulesStoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/SalesRule/CartRulesStoreConfigTest.php index 5aff568d9e81..cd6c6931ad68 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/SalesRule/CartRulesStoreConfigTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/SalesRule/CartRulesStoreConfigTest.php @@ -7,48 +7,52 @@ namespace Magento\GraphQl\SalesRule; -use Exception; use Magento\TestFramework\Fixture\Config; use Magento\TestFramework\TestCase\GraphQlAbstract; -/** - * Test coverage for Config Data from Customer->Promotion->GraphQl - */ class CartRulesStoreConfigTest extends GraphQlAbstract { - - /** - * @throws Exception - */ #[ - Config('promo/graphql/share_all_sales_rule', 1), - Config('promo/graphql/share_applied_sales_rule', 1) + Config('promo/graphql/share_applied_cart_rule', true) ] public function testCartRulesGraphQlStoreConfig(): void { $this->assertEquals( [ 'storeConfig' => [ - 'share_all_sales_rule' => 1, - 'share_applied_sales_rule' => 1, - ], + 'share_applied_cart_rule' => true + ] + ], + $this->graphQlQuery($this->getStoreConfigQuery()) + ); + } + + #[ + Config('promo/graphql/share_applied_cart_rule', false) + ] + public function testCartRulesGraphQlStoreConfigDisabled(): void + { + $this->assertEquals( + [ + 'storeConfig' => [ + 'share_applied_cart_rule' => false + ] ], - $this->graphQlQuery($this->getQuery()) + $this->graphQlQuery($this->getStoreConfigQuery()) ); } /** - * Generates storeConfig query with configurations from promo->graphql + * Generates storeConfig query with newly added configurations * * @return string */ - private function getQuery(): string + private function getStoreConfigQuery(): string { return <<fixtures = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage(); - } - - /** - * Test to retrieve all cart rules when promo/graphql/share_all_sales_rule is enabled. - * - * @throws Exception - */ - #[ - Config('promo/graphql/share_all_sales_rule', 1), - DataFixture(SalesRuleFixture::class, as: 'rule1'), - DataFixture(SalesRuleFixture::class, as: 'rule2'), - DataFixture(SalesRuleFixture::class, as: 'rule3'), - DataFixture(SalesRuleFixture::class, ['is_active' => 0], as: 'rule4') - ] - public function testGetAllCartRules(): void - { - $this->assertEmpty( - array_diff( - array_column($this->graphQlQuery($this->getAllSalesRulesQuery()), 'name'), - array_column($this->fetchAllSalesRules(), 'name') - ) - ); - } - - /** - * Test to retrieve all sales rules when promo/graphql/share_all_sales_rule is disabled. - * - * @throws Exception - */ - #[ - Config('promo/graphql/share_all_sales_rule', 0) - ] - public function testGetAllCartRulesWhenConfigDisabled(): void - { - $this->expectException(ResponseContainsErrorsException::class); - $this->expectExceptionMessage( - "Sharing Cart Rules information is disabled or not configured." - ); - $this->graphQlQuery($this->getAllSalesRulesQuery()); - } - - /** - * Get all sales rules - * - * @return array[] - */ - private function fetchAllSalesRules(): array - { - return [ - "allCartRules" => [ - ['name' => $this->fixtures->get('rule1')->getName()], - ['name' => $this->fixtures->get('rule2')->getName()], - ['name' => $this->fixtures->get('rule3')->getName()] - ] - ]; - } - - /** - * Get all sales rules query - * - * @return string - */ - private function getAllSalesRulesQuery(): string - { - return <<fixtures = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage(); + $this->idEncoder = Bootstrap::getObjectManager()->get(Uid::class); } /** - * Test to retrieve applied cart rules when promo/graphql/share_applied_sales_rule is enabled. - * - * @throws Exception + * Test to retrieve applied cart rules when promo/graphql/share_applied_cart_rule is enabled. */ #[ - Config('promo/graphql/share_applied_sales_rule', 1), - Config('sales/multicoupon/maximum_number_of_coupons_per_order', 2), + Config('promo/graphql/share_applied_cart_rule', true), + Config('sales/multicoupon/maximum_number_of_coupons_per_order', '2'), DataFixture(SalesRuleFixture::class, [ 'coupon_type' => SalesRule::COUPON_TYPE_SPECIFIC, - 'coupon_code' => self::COUPON_1, - 'stop_rules_processing' => false, + 'coupon_code' => 'COUPON_1', + 'sort_order' => 10, + 'stop_rules_processing' => false ], as: 'rule1'), DataFixture(SalesRuleFixture::class, [ 'coupon_type' => SalesRule::COUPON_TYPE_NO_COUPON, - 'stop_rules_processing' => false, + 'sort_order' => 20, + 'stop_rules_processing' => false ], as: 'rule2'), - DataFixture(SalesRuleFixture::class, [ - 'coupon_type' => SalesRule::COUPON_TYPE_SPECIFIC, - 'coupon_code' => self::COUPON_3, - ], as: 'rule3'), - DataFixture(SalesRuleFixture::class, ['is_active' => 0], as: 'rule4'), + DataFixture(SalesRuleFixture::class, ['is_active' => 0, 'sort_order' => 30], as: 'rule3'), DataFixture(ProductFixture::class, as: 'product'), DataFixture(GuestCart::class, as: 'cart'), DataFixture(AddProductToCartFixture::class, [ @@ -77,7 +70,7 @@ public function testGetAppliedCartRules(): void { $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $this->graphQlMutation($this->getApplyCouponMutation($maskedQuoteId, self::COUPON_1)); + $this->graphQlMutation($this->getApplyCouponMutation($maskedQuoteId, 'COUPON_1')); $this->assertEquals( $this->fetchAppliedSalesRules(), @@ -86,17 +79,15 @@ public function testGetAppliedCartRules(): void } /** - * Test to retrieve applied sales rules when promo/graphql/share_applied_sales_rule is disabled. - * - * @throws Exception + * Test to retrieve applied cart rules when promo/graphql/share_applied_cart_rule is disabled. */ #[ - Config('promo/graphql/share_applied_sales_rule', 0), + Config('promo/graphql/share_applied_cart_rule', false), DataFixture(ProductFixture::class, as: 'product'), DataFixture(GuestCart::class, as: 'cart'), DataFixture(AddProductToCartFixture::class, [ 'cart_id' => '$cart.id$', - 'product_id' => '$product.id$', + 'product_id' => '$product.id$' ]), DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') ] @@ -105,15 +96,40 @@ public function testGetAllCartRulesWhenConfigDisabled(): void $this->assertEquals( [ 'cart' => [ - 'rules' => null, - ], + 'rules' => null + ] + ], + $this->graphQlQuery($this->getCartQuery($this->fixtures->get('quoteIdMask')->getMaskedId())) + ); + } + + /** + * Test to retrieve applied cart rules when configuration is enabled but no cart rules are applied to the cart. + */ + #[ + Config('promo/graphql/share_applied_cart_rule', true), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(GuestCart::class, as: 'cart'), + DataFixture(AddProductToCartFixture::class, [ + 'cart_id' => '$cart.id$', + 'product_id' => '$product.id$' + ]), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') + ] + public function testGetAllCartRulesWhenConfigEnabledButRulesNotApplied(): void + { + $this->assertEquals( + [ + 'cart' => [ + 'rules' => [] + ] ], $this->graphQlQuery($this->getCartQuery($this->fixtures->get('quoteIdMask')->getMaskedId())) ); } /** - * Get applied sales rules + * Get applied cart rules * * @return array[] */ @@ -123,10 +139,10 @@ private function fetchAppliedSalesRules(): array 'cart' => [ 'rules' => [ [ - 'name' => $this->fixtures->get('rule1')->getName() + 'uid' => $this->idEncoder->encode($this->fixtures->get('rule1')->getId()) ], [ - 'name' => $this->fixtures->get('rule2')->getName() + 'uid' => $this->idEncoder->encode($this->fixtures->get('rule2')->getId()) ] ] ] @@ -157,7 +173,7 @@ private function getApplyCouponMutation(string $cartId, string $couponCode): str } /** - * Get all sales rules query + * Get applied cart rules query * * @param string $cartId * @return string @@ -168,7 +184,7 @@ private function getCartQuery(string $cartId): string query Cart { cart(cart_id: "{$cartId}") { rules { - name + uid } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddWishlistItemsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddWishlistItemsToCartTest.php index 26daa2669ab8..c5321ab7f841 100755 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddWishlistItemsToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddWishlistItemsToCartTest.php @@ -68,12 +68,11 @@ public function testAddIncompleteItemsToCart(): void $query = $this->getQuery($wishlistId, $itemId); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); - $this->assertArrayHasKey('addWishlistItemsToCart', $response); $wishlistAfterAddingToCart = $response['addWishlistItemsToCart']['wishlist']; $userErrors = $response['addWishlistItemsToCart']['add_wishlist_items_to_cart_user_errors']; $this->assertEquals($userErrors[0]['message'], 'You need to choose options for your item.'); - $this->assertEquals($userErrors[0]['code'], 'UNDEFINED'); + $this->assertEquals($userErrors[0]['code'], 'REQUIRED_PARAMETER_MISSING'); $this->assertEquals($userErrors[0]['wishlistId'], $wishlistId); $this->assertEquals($userErrors[0]['wishlistItemId'], $itemId); $wishlistItems = $wishlistAfterAddingToCart['items_v2']['items']; @@ -296,7 +295,6 @@ private function getQuery( * Returns GraphQl mutation string * * @param string $wishlistId - * @param string $itemId * @return string */ private function getAddAllItemsToCartQuery( @@ -385,8 +383,8 @@ private function getCustomerWishlistQuery(): string * Returns the GraphQl mutation string for products added to wishlist * * @param string $wishlistId - * @param string $sku2 - * @param int $quantity2 + * @param string $sku + * @param int $quantity * @return string */ private function addSecondProductToWishlist( diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/State/GraphQlStateDiff.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/GraphQlStateDiff.php index cc1e8ebae246..66588cd2b988 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQl/App/State/GraphQlStateDiff.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/GraphQlStateDiff.php @@ -1,7 +1,7 @@ doRequest($query, $authInfo); $this->objectManagerForTest->_resetState(); $this->comparator->rememberObjectsStateAfter($firstRequest); - $result = $this->comparator->compareBetweenRequests($operationName); + $result = $this->handleRequestProperties($this->comparator->compareBetweenRequests($operationName)); $test->assertEmpty( $result, sprintf( @@ -202,7 +202,7 @@ private function request( var_export($result, true) ) ); - $result = $this->comparator->compareConstructedAgainstCurrent($operationName); + $result = $this->handleRequestProperties($this->comparator->compareConstructedAgainstCurrent($operationName)); $test->assertEmpty( $result, sprintf( @@ -325,4 +325,18 @@ public function getResetPasswordToken(string $email): string $customerSecure = $customerRegistry->retrieveSecureData(1); return $customerSecure->getRpToken(); } + + /** + * Handle request properties for sslOffloadHeader + * + * @param array $result + * @return array + */ + public function handleRequestProperties(array $result): array + { + if (isset($result['Magento\Framework\Webapi\Request']['properties']['sslOffloadHeader'])) { + unset($result['Magento\Framework\Webapi\Request']); + } + return $result; + } }