diff --git a/app/code/Magento/Customer/Test/Fixture/CustomerWithAddresses.php b/app/code/Magento/Customer/Test/Fixture/CustomerWithAddresses.php
new file mode 100644
index 000000000000..1739a118315a
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Fixture/CustomerWithAddresses.php
@@ -0,0 +1,225 @@
+ null,
+ AddressInterface::CUSTOMER_ID => null,
+ AddressInterface::REGION => 'California',
+ AddressInterface::REGION_ID => '12',
+ AddressInterface::COUNTRY_ID => 'US',
+ AddressInterface::STREET => ['%street_number% Test Street%uniqid%'],
+ AddressInterface::COMPANY => null,
+ AddressInterface::TELEPHONE => '1234567893',
+ AddressInterface::FAX => null,
+ AddressInterface::POSTCODE => '02108',
+ AddressInterface::CITY => 'Boston',
+ AddressInterface::FIRSTNAME => 'Firstname%uniqid%',
+ AddressInterface::LASTNAME => 'Lastname%uniqid%',
+ AddressInterface::MIDDLENAME => null,
+ AddressInterface::PREFIX => null,
+ AddressInterface::SUFFIX => null,
+ AddressInterface::VAT_ID => null,
+ AddressInterface::DEFAULT_BILLING => true,
+ AddressInterface::DEFAULT_SHIPPING => true,
+ AddressInterface::CUSTOM_ATTRIBUTES => [],
+ AddressInterface::EXTENSION_ATTRIBUTES_KEY => [],
+ ],
+ [
+ AddressInterface::ID => null,
+ AddressInterface::CUSTOMER_ID => null,
+ AddressInterface::REGION => 'California',
+ AddressInterface::REGION_ID => '12',
+ AddressInterface::COUNTRY_ID => 'US',
+ AddressInterface::STREET => ['%street_number% Sunset Boulevard%uniqid%'],
+ AddressInterface::COMPANY => null,
+ AddressInterface::TELEPHONE => '0987654321',
+ AddressInterface::FAX => null,
+ AddressInterface::POSTCODE => '90001',
+ AddressInterface::CITY => 'Los Angeles',
+ AddressInterface::FIRSTNAME => 'Firstname%uniqid%',
+ AddressInterface::LASTNAME => 'Lastname%uniqid%',
+ AddressInterface::MIDDLENAME => null,
+ AddressInterface::PREFIX => null,
+ AddressInterface::SUFFIX => null,
+ AddressInterface::VAT_ID => null,
+ AddressInterface::DEFAULT_BILLING => false,
+ AddressInterface::DEFAULT_SHIPPING => false,
+ AddressInterface::CUSTOM_ATTRIBUTES => [],
+ AddressInterface::EXTENSION_ATTRIBUTES_KEY => [],
+ ],
+ [
+ AddressInterface::ID => null,
+ AddressInterface::CUSTOMER_ID => null,
+ AddressInterface::REGION => 'New York',
+ AddressInterface::REGION_ID => '43',
+ AddressInterface::COUNTRY_ID => 'US',
+ AddressInterface::STREET => ['%street_number% 5th Avenue%uniqid%'],
+ AddressInterface::COMPANY => null,
+ AddressInterface::TELEPHONE => '1112223333',
+ AddressInterface::FAX => null,
+ AddressInterface::POSTCODE => '10001',
+ AddressInterface::CITY => 'New York City',
+ AddressInterface::FIRSTNAME => 'Firstname%uniqid%',
+ AddressInterface::LASTNAME => 'Lastname%uniqid%',
+ AddressInterface::MIDDLENAME => null,
+ AddressInterface::PREFIX => null,
+ AddressInterface::SUFFIX => null,
+ AddressInterface::VAT_ID => null,
+ AddressInterface::DEFAULT_BILLING => false,
+ AddressInterface::DEFAULT_SHIPPING => false,
+ AddressInterface::CUSTOM_ATTRIBUTES => [],
+ AddressInterface::EXTENSION_ATTRIBUTES_KEY => [],
+ ]
+ ];
+
+ private const DEFAULT_DATA = [
+ 'password' => 'password',
+ CustomerInterface::ID => null,
+ CustomerInterface::CREATED_AT => null,
+ CustomerInterface::CONFIRMATION => null,
+ CustomerInterface::UPDATED_AT => null,
+ CustomerInterface::DOB => null,
+ CustomerInterface::CREATED_IN => null,
+ CustomerInterface::EMAIL => 'customer%uniqid%@mail.com',
+ CustomerInterface::FIRSTNAME => 'Firstname%uniqid%',
+ CustomerInterface::GROUP_ID => null,
+ CustomerInterface::GENDER => null,
+ CustomerInterface::LASTNAME => 'Lastname%uniqid%',
+ CustomerInterface::MIDDLENAME => null,
+ CustomerInterface::PREFIX => null,
+ CustomerInterface::SUFFIX => null,
+ CustomerInterface::STORE_ID => null,
+ CustomerInterface::TAXVAT => null,
+ CustomerInterface::WEBSITE_ID => null,
+ CustomerInterface::DEFAULT_SHIPPING => null,
+ CustomerInterface::DEFAULT_BILLING => null,
+ CustomerInterface::DISABLE_AUTO_GROUP_CHANGE => null,
+ CustomerInterface::KEY_ADDRESSES => [],
+ CustomerInterface::CUSTOM_ATTRIBUTES => [],
+ CustomerInterface::EXTENSION_ATTRIBUTES_KEY => [],
+ ];
+
+ /**
+ * CustomerWithAddresses Constructor
+ *
+ * @param ServiceFactory $serviceFactory
+ * @param AccountManagementInterface $accountManagement
+ * @param CustomerRegistry $customerRegistry
+ * @param ProcessorInterface $dataProcessor
+ * @param DataMerger $dataMerger
+ */
+ public function __construct(
+ private readonly ServiceFactory $serviceFactory,
+ private readonly AccountManagementInterface $accountManagement,
+ private readonly CustomerRegistry $customerRegistry,
+ private readonly DataMerger $dataMerger,
+ private readonly ProcessorInterface $dataProcessor
+ ) {
+ }
+
+ /**
+ * Apply the changes for the fixture
+ *
+ * @param array $data
+ * @return DataObject|null
+ * @throws NoSuchEntityException
+ */
+ public function apply(array $data = []): ?DataObject
+ {
+ $customerSave = $this->serviceFactory->create(CustomerRepositoryInterface::class, 'save');
+ $data = $this->prepareCustomerData($data);
+ $passwordHash = $this->accountManagement->getPasswordHash($data['password']);
+ unset($data['password']);
+
+ $customerSave->execute(
+ [
+ 'customer' => $data,
+ 'passwordHash' => $passwordHash
+ ]
+ );
+
+ return $this->customerRegistry->retrieveByEmail($data['email'], $data['website_id']);
+ }
+
+ /**
+ * Revert the test customer creation
+ *
+ * @param DataObject $data
+ * @return void
+ */
+ public function revert(DataObject $data): void
+ {
+ $data->setCustomerId($data->getId());
+ $customerService = $this->serviceFactory->create(CustomerRepositoryInterface::class, 'deleteById');
+
+ $customerService->execute(
+ [
+ 'customerId' => $data->getId()
+ ]
+ );
+ }
+
+ /**
+ * Prepare customer's data
+ *
+ * @param array $data
+ * @return array
+ */
+ private function prepareCustomerData(array $data): array
+ {
+ $data = $this->dataMerger->merge(self::DEFAULT_DATA, $data);
+ $data[CustomerInterface::KEY_ADDRESSES] = $this->prepareAddresses($data[CustomerInterface::KEY_ADDRESSES]);
+
+ return $this->dataProcessor->process($this, $data);
+ }
+
+ /**
+ * Prepare customer's addresses
+ *
+ * @param array $data
+ * @return array
+ */
+ private function prepareAddresses(array $data): array
+ {
+ $addressesData = [];
+ $default = self::DEFAULT_DATA_ADDRESS;
+ $streetNumber = 123;
+ foreach ($default as $address) {
+ if ($data) {
+ $address = $this->dataMerger->merge($default, $address);
+ }
+ $placeholders = ['%street_number%' => $streetNumber++];
+ $address[AddressInterface::STREET] = array_map(
+ fn ($str) => strtr($str, $placeholders),
+ $address[AddressInterface::STREET]
+ );
+ $addressesData[] = $address;
+ }
+
+ return $addressesData;
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Formatter/CustomerAddresses.php b/app/code/Magento/CustomerGraphQl/Model/Formatter/CustomerAddresses.php
new file mode 100644
index 000000000000..7c3a0b91dade
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Formatter/CustomerAddresses.php
@@ -0,0 +1,49 @@
+getItems() as $address) {
+ $addressArray[] = $this->extractCustomerAddressData->execute($address);
+ }
+
+ return [
+ 'total_count' => $searchResult->getTotalCount(),
+ 'items' => $addressArray,
+ 'page_info' => [
+ 'page_size' => $searchResult->getSearchCriteria()->getPageSize(),
+ 'current_page' => $searchResult->getSearchCriteria()->getCurrentPage(),
+ 'total_pages' => (int)ceil($searchResult->getTotalCount()
+ / (int)$searchResult->getSearchCriteria()->getPageSize()),
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddressesV2.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddressesV2.php
new file mode 100644
index 000000000000..b7f54ec60a66
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddressesV2.php
@@ -0,0 +1,68 @@
+validateAddressRequest->execute($value, $args);
+
+ /** @var Customer $customer */
+ $customer = $value['model'];
+
+ try {
+ $this->searchCriteriaBuilder->addFilter('parent_id', (int)$customer->getId());
+ $this->searchCriteriaBuilder->setCurrentPage($args['currentPage']);
+ $this->searchCriteriaBuilder->setPageSize($args['pageSize']);
+ $searchResult = $this->addressRepository->getList($this->searchCriteriaBuilder->create());
+ } catch (InputException $e) {
+ throw new GraphQlInputException(__($e->getMessage()));
+ }
+
+ return $this->addressesFormatter->format($searchResult);
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/ValidateAddressRequest.php b/app/code/Magento/CustomerGraphQl/Model/ValidateAddressRequest.php
new file mode 100644
index 000000000000..433da7209af6
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/ValidateAddressRequest.php
@@ -0,0 +1,38 @@
+
@@ -104,7 +115,7 @@
20
- 300
+ 1000
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/CancelOrderGuest.php b/app/code/Magento/OrderCancellationGraphQl/Model/CancelOrderGuest.php
index 038e4fa74af1..dd93816d18ee 100644
--- a/app/code/Magento/OrderCancellationGraphQl/Model/CancelOrderGuest.php
+++ b/app/code/Magento/OrderCancellationGraphQl/Model/CancelOrderGuest.php
@@ -17,17 +17,12 @@
namespace Magento\OrderCancellationGraphQl\Model;
use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\OrderCancellation\Model\Email\ConfirmationKeySender;
use Magento\OrderCancellation\Model\GetConfirmationKey;
-use Magento\OrderCancellationGraphQl\Model\Validator\GuestOrder\ValidateRequest;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Model\Order;
use Magento\SalesGraphQl\Model\Formatter\Order as OrderFormatter;
-/**
- * Class for Guest order cancellation
- */
class CancelOrderGuest
{
/**
@@ -35,14 +30,12 @@ class CancelOrderGuest
*
* @param OrderFormatter $orderFormatter
* @param OrderRepositoryInterface $orderRepository
- * @param ValidateRequest $validateRequest
* @param ConfirmationKeySender $confirmationKeySender
* @param GetConfirmationKey $confirmationKey
*/
public function __construct(
private readonly OrderFormatter $orderFormatter,
private readonly OrderRepositoryInterface $orderRepository,
- private readonly ValidateRequest $validateRequest,
private readonly ConfirmationKeySender $confirmationKeySender,
private readonly GetConfirmationKey $confirmationKey,
) {
@@ -54,13 +47,9 @@ public function __construct(
* @param Order $order
* @param array $input
* @return array
- * @throws GraphQlInputException
- * @throws LocalizedException
*/
public function execute(Order $order, array $input): array
{
- $this->validateRequest->validateCancelGuestOrderInput($input);
-
try {
// send confirmation key and order id
$this->sendConfirmationKeyEmail($order, $input['reason']);
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/ConfirmCancelOrderGuest.php b/app/code/Magento/OrderCancellationGraphQl/Model/ConfirmCancelOrder.php
similarity index 95%
rename from app/code/Magento/OrderCancellationGraphQl/Model/ConfirmCancelOrderGuest.php
rename to app/code/Magento/OrderCancellationGraphQl/Model/ConfirmCancelOrder.php
index 4f9e7979ab9b..e627d4fa0310 100644
--- a/app/code/Magento/OrderCancellationGraphQl/Model/ConfirmCancelOrderGuest.php
+++ b/app/code/Magento/OrderCancellationGraphQl/Model/ConfirmCancelOrder.php
@@ -19,18 +19,17 @@
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\OrderCancellation\Model\CancelOrder as CancelOrderAction;
-use Magento\OrderCancellation\Model\ResourceModel\SalesOrderConfirmCancel
- as SalesOrderConfirmCancelResourceModel;
+use Magento\OrderCancellation\Model\ResourceModel\SalesOrderConfirmCancel as SalesOrderConfirmCancelResourceModel;
use Magento\Sales\Model\Order;
use Magento\SalesGraphQl\Model\Formatter\Order as OrderFormatter;
/**
* Class for Guest order cancellation confirmation
*/
-class ConfirmCancelOrderGuest
+class ConfirmCancelOrder
{
/**
- * ConfirmCancelOrderGuest Constructor
+ * ConfirmCancelOrder Constructor
*
* @param OrderFormatter $orderFormatter
* @param CancelOrderAction $cancelOrderAction
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancelOrder.php b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancelOrder.php
index 86a381efc54b..c1e2f3201411 100644
--- a/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancelOrder.php
+++ b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancelOrder.php
@@ -18,15 +18,12 @@
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
+use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\OrderCancellation\Model\CancelOrder as CancelOrderAction;
-use Magento\OrderCancellation\Model\Config\Config;
-use Magento\OrderCancellationGraphQl\Model\CancelOrderGuest;
-use Magento\OrderCancellationGraphQl\Model\Validator\ValidateCustomer;
use Magento\OrderCancellationGraphQl\Model\Validator\ValidateOrder;
use Magento\OrderCancellationGraphQl\Model\Validator\ValidateRequest;
-use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\SalesGraphQl\Model\Formatter\Order as OrderFormatter;
@@ -42,20 +39,14 @@ class CancelOrder implements ResolverInterface
* @param OrderFormatter $orderFormatter
* @param OrderRepositoryInterface $orderRepository
* @param CancelOrderAction $cancelOrderAction
- * @param CancelOrderGuest $cancelOrderGuest
* @param ValidateOrder $validateOrder
- * @param ValidateCustomer $validateCustomer
- * @param Config $config
*/
public function __construct(
private readonly ValidateRequest $validateRequest,
private readonly OrderFormatter $orderFormatter,
private readonly OrderRepositoryInterface $orderRepository,
private readonly CancelOrderAction $cancelOrderAction,
- private readonly CancelOrderGuest $cancelOrderGuest,
- private readonly ValidateOrder $validateOrder,
- private readonly ValidateCustomer $validateCustomer,
- private readonly Config $config
+ private readonly ValidateOrder $validateOrder
) {
}
@@ -69,12 +60,13 @@ public function resolve(
array $value = null,
array $args = null
) {
- $this->validateRequest->execute($args['input'] ?? []);
+ $this->validateRequest->execute($context, $args['input'] ?? []);
try {
$order = $this->orderRepository->get($args['input']['order_id']);
- if (!$this->isOrderCancellationEnabled($order)) {
- return $this->createErrorResponse('Order cancellation is not enabled for requested store.');
+
+ if ((int)$order->getCustomerId() !== $context->getUserId()) {
+ throw new GraphQlAuthorizationException(__('Current user is not authorized to cancel this order'));
}
$errors = $this->validateOrder->execute($order);
@@ -82,48 +74,16 @@ public function resolve(
return $errors;
}
- if ($order->getCustomerIsGuest()) {
- return $this->cancelOrderGuest->execute($order, $args['input']);
- }
-
- $this->validateCustomer->execute($order, $context);
-
$order = $this->cancelOrderAction->execute($order, $args['input']['reason']);
return [
'order' => $this->orderFormatter->format($order)
];
- } catch (LocalizedException $e) {
- return $this->createErrorResponse($e->getMessage());
- }
- }
- /**
- * Create error response
- *
- * @param string $message
- * @param OrderInterface|null $order
- * @return array
- * @throws LocalizedException
- */
- private function createErrorResponse(string $message, OrderInterface $order = null): array
- {
- $response = ['error' => __($message)];
- if ($order) {
- $response['order'] = $this->orderFormatter->format($order);
+ } catch (LocalizedException $e) {
+ return [
+ 'error' => __($e->getMessage())
+ ];
}
-
- return $response;
- }
-
- /**
- * Check if order cancellation is enabled in config
- *
- * @param OrderInterface $order
- * @return bool
- */
- private function isOrderCancellationEnabled(OrderInterface $order): bool
- {
- return $this->config->isOrderCancellationEnabledForStore((int)$order->getStoreId());
}
}
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/ConfirmCancelOrder.php b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/ConfirmCancelOrder.php
new file mode 100644
index 000000000000..f6a171404548
--- /dev/null
+++ b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/ConfirmCancelOrder.php
@@ -0,0 +1,82 @@
+validateRequest->execute($args['input'] ?? []);
+
+ try {
+ $order = $this->orderRepository->get($args['input']['order_id']);
+
+ if (!$order->getCustomerIsGuest()) {
+ return [
+ 'error' => __('Current user is not authorized to cancel this order')
+ ];
+ }
+
+ $errors = $this->validateOrder->execute($order);
+ if (!empty($errors)) {
+ return $errors;
+ }
+
+ return $this->confirmCancelOrder->execute($order, $args['input']);
+ } catch (LocalizedException $e) {
+ return [
+ 'error' => __($e->getMessage())
+ ];
+ }
+ }
+}
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/ConfirmCancelOrderGuest.php b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/ConfirmCancelOrderGuest.php
deleted file mode 100644
index d067855bbd28..000000000000
--- a/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/ConfirmCancelOrderGuest.php
+++ /dev/null
@@ -1,88 +0,0 @@
-validateRequest->execute($args['input'] ?? []);
-
- $order = $this->loadOrder((int)$args['input']['order_id']);
- $errors = $this->validateOrder->execute($order);
- if (!empty($errors)) {
- return $errors;
- }
-
- return $this->confirmCancelOrderGuest->execute($order, $args['input']);
- }
-
- /**
- * Load order interface from order id
- *
- * @param int $orderId
- * @return Order
- * @throws LocalizedException
- */
- private function loadOrder(int $orderId): Order
- {
- try {
- return $this->orderRepository->get($orderId);
- } catch (Exception $e) {
- throw new GraphQlInputException(__($e->getMessage()));
- }
- }
-}
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/RequestGuestOrderCancel.php b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/RequestGuestOrderCancel.php
new file mode 100644
index 000000000000..f1315c123900
--- /dev/null
+++ b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/RequestGuestOrderCancel.php
@@ -0,0 +1,119 @@
+validateRequest->validateInput($args['input'] ?? []);
+ list($number, $email, $postcode) = $this->getNumberEmailPostcode($args['input']['token']);
+
+ $order = $this->getOrder($number);
+ $this->validateRequest->validateOrderDetails($order, $postcode, $email);
+
+ $errors = $this->validateOrder->execute($order);
+ if ($errors) {
+ return $errors;
+ }
+
+ return $this->cancelOrderGuest->execute($order, $args['input']);
+ }
+
+ /**
+ * Retrieve order details based on order number
+ *
+ * @param string $number
+ * @return OrderInterface
+ * @throws GraphQlNoSuchEntityException
+ * @throws NoSuchEntityException
+ */
+ private function getOrder(string $number): OrderInterface
+ {
+ $searchCriteria = $this->searchCriteriaBuilderFactory->create()
+ ->addFilter('increment_id', $number)
+ ->addFilter('store_id', $this->storeManager->getStore()->getId())
+ ->create();
+
+ $orders = $this->orderRepository->getList($searchCriteria)->getItems();
+ if (empty($orders)) {
+ $this->validateRequest->cannotLocateOrder();
+ }
+
+ return reset($orders);
+ }
+
+ /**
+ * Retrieve number, email and postcode from token
+ *
+ * @param string $token
+ * @return array
+ * @throws GraphQlNoSuchEntityException
+ */
+ private function getNumberEmailPostcode(string $token): array
+ {
+ $data = $this->token->decrypt($token);
+ if (count($data) !== 3) {
+ $this->validateRequest->cannotLocateOrder();
+ }
+ return $data;
+ }
+}
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Validator/GuestOrder/ValidateOrder.php b/app/code/Magento/OrderCancellationGraphQl/Model/Validator/GuestOrder/ValidateOrder.php
deleted file mode 100644
index 485cc6577ec2..000000000000
--- a/app/code/Magento/OrderCancellationGraphQl/Model/Validator/GuestOrder/ValidateOrder.php
+++ /dev/null
@@ -1,81 +0,0 @@
-config->isOrderCancellationEnabledForStore((int)$order->getStoreId())) {
- return [
- 'error' => __('Order cancellation is not enabled for requested store.')
- ];
- }
-
- if (!$order->getCustomerIsGuest()) {
- return [
- 'error' => __('Current user is not authorized to cancel this order')
- ];
- }
-
- if (!$this->canCancelOrder->execute($order)) {
- return [
- 'error' => __('Order already closed, complete, cancelled or on hold'),
- 'order' => $this->orderFormatter->format($order)
- ];
- }
-
- if ($order->hasShipments()) {
- return [
- 'error' => __('Order with one or more items shipped cannot be cancelled'),
- 'order' => $this->orderFormatter->format($order)
- ];
- }
-
- return [];
- }
-}
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Validator/GuestOrder/ValidateRequest.php b/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateConfirmRequest.php
similarity index 75%
rename from app/code/Magento/OrderCancellationGraphQl/Model/Validator/GuestOrder/ValidateRequest.php
rename to app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateConfirmRequest.php
index 8a7e8cad1ff4..e6012f56f7e7 100644
--- a/app/code/Magento/OrderCancellationGraphQl/Model/Validator/GuestOrder/ValidateRequest.php
+++ b/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateConfirmRequest.php
@@ -14,7 +14,7 @@
*/
declare(strict_types=1);
-namespace Magento\OrderCancellationGraphQl\Model\Validator\GuestOrder;
+namespace Magento\OrderCancellationGraphQl\Model\Validator;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\OrderCancellation\Model\GetConfirmationKey;
@@ -22,7 +22,7 @@
/**
* Ensure all conditions to cancel guest order are met
*/
-class ValidateRequest
+class ValidateConfirmRequest
{
/**
* Ensure the input to cancel guest order is valid
@@ -64,25 +64,4 @@ public function execute(mixed $input): void
);
}
}
-
- /**
- * Validate cancel guest order input
- *
- * @param array $input
- * @return void
- * @throws GraphQlInputException
- */
- public function validateCancelGuestOrderInput(array $input): void
- {
- if (!$input['reason'] || !is_string($input['reason'])) {
- throw new GraphQlInputException(
- __(
- 'Required parameter "%field" is missing or incorrect.',
- [
- 'field' => 'reason'
- ]
- )
- );
- }
- }
}
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateCustomer.php b/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateCustomer.php
deleted file mode 100644
index 0bd8887b914f..000000000000
--- a/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateCustomer.php
+++ /dev/null
@@ -1,43 +0,0 @@
-getExtensionAttributes()->getIsCustomer() === false) {
- throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.'));
- }
-
- if ((int)$order->getCustomerId() !== $context->getUserId()) {
- throw new GraphQlAuthorizationException(__('Current user is not authorized to cancel this order'));
- }
- }
-}
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateGuestRequest.php b/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateGuestRequest.php
new file mode 100644
index 000000000000..b3d7eafed587
--- /dev/null
+++ b/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateGuestRequest.php
@@ -0,0 +1,100 @@
+ 'token'
+ ]
+ )
+ );
+ }
+
+ if (!$input['reason'] || !is_string($input['reason'])) {
+ throw new GraphQlInputException(
+ __(
+ 'Required parameter "%field" is missing or incorrect.',
+ [
+ 'field' => 'reason'
+ ]
+ )
+ );
+ }
+ }
+
+ /**
+ * Ensure the order matches the provided criteria
+ *
+ * @param OrderInterface $order
+ * @param string $postcode
+ * @param string $email
+ * @return void
+ * @throws GraphQlAuthorizationException
+ * @throws GraphQlNoSuchEntityException
+ */
+ public function validateOrderDetails(OrderInterface $order, string $postcode, string $email): void
+ {
+ $billingAddress = $order->getBillingAddress();
+
+ if ($billingAddress->getPostcode() !== $postcode || $billingAddress->getEmail() !== $email) {
+ $this->cannotLocateOrder();
+ }
+
+ if ($order->getCustomerId()) {
+ throw new GraphQlAuthorizationException(__('Please login to view the order.'));
+ }
+ }
+
+ /**
+ * Throw exception when the order cannot be found or does not match the criteria
+ *
+ * @return void
+ * @throws GraphQlNoSuchEntityException
+ */
+ public function cannotLocateOrder(): void
+ {
+ throw new GraphQlNoSuchEntityException(__('We couldn\'t locate an order with the information provided.'));
+ }
+}
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateOrder.php b/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateOrder.php
index ebc9befc8a0f..5f3736cd967f 100644
--- a/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateOrder.php
+++ b/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateOrder.php
@@ -17,6 +17,7 @@
namespace Magento\OrderCancellationGraphQl\Model\Validator;
use Magento\Framework\Exception\LocalizedException;
+use Magento\OrderCancellation\Model\Config\Config;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\OrderCancellation\Model\CustomerCanCancel;
use Magento\SalesGraphQl\Model\Formatter\Order as OrderFormatter;
@@ -28,10 +29,12 @@ class ValidateOrder
*
* @param CustomerCanCancel $customerCanCancel
* @param OrderFormatter $orderFormatter
+ * @param Config $config
*/
public function __construct(
private readonly CustomerCanCancel $customerCanCancel,
- private readonly OrderFormatter $orderFormatter
+ private readonly OrderFormatter $orderFormatter,
+ private readonly Config $config
) {
}
@@ -44,6 +47,12 @@ public function __construct(
*/
public function execute(OrderInterface $order): array
{
+ if (!$this->config->isOrderCancellationEnabledForStore((int)$order->getStoreId())) {
+ return [
+ 'error' => __('Order cancellation is not enabled for requested store.')
+ ];
+ }
+
if (!$this->customerCanCancel->execute($order)) {
return [
'error' => __('Order already closed, complete, cancelled or on hold'),
diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateRequest.php b/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateRequest.php
index 6d426c470a23..1ef73c99f9ce 100644
--- a/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateRequest.php
+++ b/app/code/Magento/OrderCancellationGraphQl/Model/Validator/ValidateRequest.php
@@ -16,7 +16,9 @@
namespace Magento\OrderCancellationGraphQl\Model\Validator;
+use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
+use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
/**
* Ensure all conditions to cancel order are met
@@ -26,13 +28,19 @@ class ValidateRequest
/**
* Ensure customer is authorized and the field is populated
*
+ * @param ContextInterface $context
* @param array|null $input
* @return void
- * @throws GraphQlInputException
+ * @throws GraphQlInputException|GraphQlAuthorizationException
*/
public function execute(
+ ContextInterface $context,
?array $input,
): void {
+ if ($context->getExtensionAttributes()->getIsCustomer() === false) {
+ throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.'));
+ }
+
if (!is_array($input) || empty($input)) {
throw new GraphQlInputException(
__('CancelOrderInput is missing.')
@@ -50,7 +58,7 @@ public function execute(
);
}
- if (!$input['reason'] || !is_string($input['reason']) || (string)$input['reason'] === "") {
+ if (!$input['reason'] || !is_string($input['reason'])) {
throw new GraphQlInputException(
__(
'Required parameter "%field" is missing or incorrect.',
diff --git a/app/code/Magento/OrderCancellationGraphQl/composer.json b/app/code/Magento/OrderCancellationGraphQl/composer.json
index d38fc3a2dc3e..a1da56ef74a5 100644
--- a/app/code/Magento/OrderCancellationGraphQl/composer.json
+++ b/app/code/Magento/OrderCancellationGraphQl/composer.json
@@ -7,6 +7,7 @@
"require": {
"php": "~8.1.0||~8.2.0||~8.3.0",
"magento/framework": "*",
+ "magento/module-store": "*",
"magento/module-sales": "*",
"magento/module-sales-graph-ql": "*",
"magento/module-order-cancellation": "*"
diff --git a/app/code/Magento/OrderCancellationGraphQl/etc/schema.graphqls b/app/code/Magento/OrderCancellationGraphQl/etc/schema.graphqls
index e0d8f38c432d..a323ca635595 100644
--- a/app/code/Magento/OrderCancellationGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/OrderCancellationGraphQl/etc/schema.graphqls
@@ -1,5 +1,6 @@
-# Copyright © Magento, Inc. All rights reserved.
-# See COPYING.txt for license details.
+# Copyright 2024 Adobe
+# All Rights Reserved.
+
type StoreConfig {
order_cancellation_enabled: Boolean! @doc(description: "Indicates whether orders can be cancelled by customers or not.")
order_cancellation_reasons: [CancellationReason!]! @resolver(class: "Magento\\OrderCancellationGraphQl\\Model\\Resolver\\CancellationReasons") @doc(description: "An array containing available cancellation reasons.")
@@ -11,8 +12,8 @@ type CancellationReason {
type Mutation {
cancelOrder(input: CancelOrderInput!): CancelOrderOutput @resolver(class: "Magento\\OrderCancellationGraphQl\\Model\\Resolver\\CancelOrder") @doc(description: "Cancel the specified customer order.")
- confirmCancelOrder(input: ConfirmCancelOrderInput!): CancelOrderOutput @resolver(class: "Magento\\OrderCancellationGraphQl\\Model\\Resolver\\ConfirmCancelOrderGuest") @doc(description: "Cancel the specified guest customer order.")
-
+ confirmCancelOrder(input: ConfirmCancelOrderInput!): CancelOrderOutput @resolver(class: "Magento\\OrderCancellationGraphQl\\Model\\Resolver\\ConfirmCancelOrder") @doc(description: "Cancel the specified guest customer order.")
+ requestGuestOrderCancel(input: GuestOrderCancelInput!): CancelOrderOutput @resolver(class: "Magento\\OrderCancellationGraphQl\\Model\\Resolver\\RequestGuestOrderCancel") @doc(description: "Request to cancel specified guest order.")
}
input CancelOrderInput @doc(description: "Defines the order to cancel.") {
@@ -25,6 +26,11 @@ input ConfirmCancelOrderInput {
confirmation_key: String! @doc(description: "Confirmation Key to cancel the order.")
}
+input GuestOrderCancelInput @doc(description: "Input to retrieve a guest order based on token.") {
+ token: String! @doc(description: "Order token.")
+ reason: String! @doc(description: "Cancellation reason.")
+}
+
type CancelOrderOutput @doc(description: "Contains the updated customer order and error message if any.") {
error: String @doc(description: "Error encountered while cancelling the order.")
order: CustomerOrder @doc(description: "Updated customer order.")
diff --git a/app/code/Magento/Quote/Api/ErrorInterface.php b/app/code/Magento/Quote/Api/ErrorInterface.php
new file mode 100644
index 000000000000..3e93c2e71087
--- /dev/null
+++ b/app/code/Magento/Quote/Api/ErrorInterface.php
@@ -0,0 +1,46 @@
+cartRepository = $cartRepository;
- $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId;
- $this->requestBuilder = $requestBuilder;
- $this->productReader = $productReader;
- $this->error = $addProductsToCartError;
}
/**
@@ -128,7 +101,16 @@ function ($item) {
);
$this->productReader->loadProducts($skus, $cart->getStoreId());
foreach ($cartItems as $cartItemPosition => $cartItem) {
- $errors = $this->addItemToCart($cart, $cartItem, $cartItemPosition);
+ $product = $this->productReader->getProductBySku($cartItem->getSku());
+ $stockItemQuantity = 0.0;
+ if ($product) {
+ $stockItem = $this->stockRegistry->getStockItem(
+ $product->getId(),
+ $cart->getStore()->getWebsiteId()
+ );
+ $stockItemQuantity = $stockItem->getQty() - $stockItem->getMinQty();
+ }
+ $errors = $this->addItemToCart($cart, $cartItem, $cartItemPosition, $stockItemQuantity);
if ($errors) {
$failedCartItems[$cartItemPosition] = $errors;
}
@@ -143,10 +125,15 @@ function ($item) {
* @param Quote $cart
* @param Data\CartItem $cartItem
* @param int $cartItemPosition
+ * @param float $stockItemQuantity
* @return array
*/
- private function addItemToCart(Quote $cart, Data\CartItem $cartItem, int $cartItemPosition): array
- {
+ private function addItemToCart(
+ Quote $cart,
+ Data\CartItem $cartItem,
+ int $cartItemPosition,
+ float $stockItemQuantity
+ ): array {
$sku = $cartItem->getSku();
$errors = [];
$result = null;
@@ -154,31 +141,37 @@ private function addItemToCart(Quote $cart, Data\CartItem $cartItem, int $cartIt
if ($cartItem->getQuantity() <= 0) {
$errors[] = $this->error->create(
__('The product quantity should be greater than 0')->render(),
- $cartItemPosition
+ $cartItemPosition,
+ $stockItemQuantity
);
- } else {
- $productBySku = $this->productReader->getProductBySku($sku);
- $product = isset($productBySku) ? clone $productBySku : null;
- if (!$product || !$product->isSaleable() || !$product->isAvailable()) {
- $errors[] = $this->error->create(
+ }
+
+ $productBySku = $this->productReader->getProductBySku($sku);
+ $product = isset($productBySku) ? clone $productBySku : null;
+
+ if (!$product || !$product->isSaleable() || !$product->isAvailable()) {
+ return [
+ $this->error->create(
__('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(),
- $cartItemPosition
- );
- } else {
- try {
- $result = $cart->addProduct($product, $this->requestBuilder->build($cartItem));
- } catch (\Throwable $e) {
- $errors[] = $this->error->create(
- __($e->getMessage())->render(),
- $cartItemPosition
- );
- }
- }
+ $cartItemPosition,
+ $stockItemQuantity
+ )
+ ];
+ }
- if (is_string($result)) {
- foreach (array_unique(explode("\n", $result)) as $error) {
- $errors[] = $this->error->create(__($error)->render(), $cartItemPosition);
- }
+ try {
+ $result = $cart->addProduct($product, $this->requestBuilder->build($cartItem));
+ } catch (\Throwable $e) {
+ $errors[] = $this->error->create(
+ __($e->getMessage())->render(),
+ $cartItemPosition,
+ $stockItemQuantity
+ );
+ }
+
+ if (is_string($result)) {
+ foreach (array_unique(explode("\n", $result)) as $error) {
+ $errors[] = $this->error->create(__($error)->render(), $cartItemPosition, $stockItemQuantity);
}
}
diff --git a/app/code/Magento/Quote/Model/Cart/AddProductsToCartError.php b/app/code/Magento/Quote/Model/Cart/AddProductsToCartError.php
index 2b014b4366f3..8b914f9d5edf 100644
--- a/app/code/Magento/Quote/Model/Cart/AddProductsToCartError.php
+++ b/app/code/Magento/Quote/Model/Cart/AddProductsToCartError.php
@@ -1,55 +1,48 @@
self::ERROR_PRODUCT_NOT_FOUND,
- 'The required options you selected are not available' => self::ERROR_NOT_SALABLE,
- 'Product that you are trying to add is not available.' => self::ERROR_NOT_SALABLE,
- 'This product is out of stock' => self::ERROR_INSUFFICIENT_STOCK,
- 'There are no source items' => self::ERROR_NOT_SALABLE,
- 'The fewest you may purchase is' => self::ERROR_INSUFFICIENT_STOCK,
- 'The most you may purchase is' => self::ERROR_INSUFFICIENT_STOCK,
- 'The requested qty is not available' => self::ERROR_INSUFFICIENT_STOCK,
- 'Not enough items for sale' => self::ERROR_INSUFFICIENT_STOCK,
- 'Only %s of %s available' => self::ERROR_INSUFFICIENT_STOCK,
- ];
+ public function __construct(
+ private readonly array $errorMessageCodesMapper
+ ) {
+ }
/**
* Returns an error object
*
* @param string $message
* @param int $cartItemPosition
+ * @param float $stockItemQuantity
* @return Data\Error
*/
- public function create(string $message, int $cartItemPosition = 0): Data\Error
- {
- return new Data\Error(
+ public function create(
+ string $message,
+ int $cartItemPosition = 0,
+ float $stockItemQuantity = 0.0
+ ): ErrorInterface {
+
+ return new Data\InsufficientStockError(
$message,
$this->getErrorCode($message),
- $cartItemPosition
+ $cartItemPosition,
+ $stockItemQuantity
);
}
@@ -62,7 +55,7 @@ public function create(string $message, int $cartItemPosition = 0): Data\Error
private function getErrorCode(string $message): string
{
$message = preg_replace('/\d+/', '%s', $message);
- foreach (self::MESSAGE_CODES as $codeMessage => $code) {
+ foreach ($this->errorMessageCodesMapper as $codeMessage => $code) {
if (false !== stripos($message, $codeMessage)) {
return $code;
}
diff --git a/app/code/Magento/Quote/Model/Cart/Data/Error.php b/app/code/Magento/Quote/Model/Cart/Data/Error.php
index 42b14b06d94a..bc5f3e1ef91d 100644
--- a/app/code/Magento/Quote/Model/Cart/Data/Error.php
+++ b/app/code/Magento/Quote/Model/Cart/Data/Error.php
@@ -1,42 +1,39 @@
message = $message;
- $this->code = $code;
- $this->cartItemPosition = $cartItemPosition;
+ public function __construct(
+ private readonly string $message,
+ private readonly string $code,
+ private readonly int $cartItemPosition
+ ) {
}
/**
diff --git a/app/code/Magento/Quote/Model/Cart/Data/InsufficientStockError.php b/app/code/Magento/Quote/Model/Cart/Data/InsufficientStockError.php
new file mode 100644
index 000000000000..4f49795692b1
--- /dev/null
+++ b/app/code/Magento/Quote/Model/Cart/Data/InsufficientStockError.php
@@ -0,0 +1,56 @@
+quantity = $quantity;
+ parent::__construct($message, $code, $cartItemPosition);
+ }
+
+ /**
+ * Get Stock quantity
+ *
+ * @return float
+ */
+ public function getQuantity(): float
+ {
+ return $this->quantity;
+ }
+}
diff --git a/app/code/Magento/Quote/etc/di.xml b/app/code/Magento/Quote/etc/di.xml
index 04be517537b0..328633b7f55e 100644
--- a/app/code/Magento/Quote/etc/di.xml
+++ b/app/code/Magento/Quote/etc/di.xml
@@ -1,8 +1,19 @@
@@ -166,4 +177,21 @@
Magento\Quote\Model\QuoteIdMutex
+
+
+
+
+ - PRODUCT_NOT_FOUND
+ - NOT_SALABLE
+ - NOT_SALABLE
+ - INSUFFICIENT_STOCK
+ - NOT_SALABLE
+ - INSUFFICIENT_STOCK
+ - INSUFFICIENT_STOCK
+ - INSUFFICIENT_STOCK
+ - INSUFFICIENT_STOCK
+ - INSUFFICIENT_STOCK
+
+
+
diff --git a/app/code/Magento/QuoteGraphQl/Model/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/AddProductsToCart.php
new file mode 100644
index 000000000000..4add0607805e
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/AddProductsToCart.php
@@ -0,0 +1,100 @@
+getExtensionAttributes()->getStore()->getId();
+
+ // Shopping Cart validation
+ $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId);
+ $cartItemsData = $this->cartItemPrecursor->process($cartItemsData, $context);
+ $cartItems = [];
+ foreach ($cartItemsData as $cartItemData) {
+ $cartItems[] = (new CartItemFactory())->create($cartItemData);
+ }
+
+ /** @var AddProductsToCartOutput $addProductsToCartOutput */
+ $addProductsToCartOutput = $this->addProductsToCartService->execute($maskedCartId, $cartItems);
+
+ return [
+ 'cart' => [
+ 'model' => $addProductsToCartOutput->getCart(),
+ ],
+ 'user_errors' => array_map(
+ function (ErrorInterface $error) {
+ return [
+ 'code' => $error->getCode(),
+ 'message' => $error->getMessage(),
+ 'path' => [$error->getCartItemPosition()],
+ 'quantity' => $this->isStockItemMessageEnabled() ? $error->getQuantity() : null
+ ];
+ },
+ array_merge($addProductsToCartOutput->getErrors(), $this->cartItemPrecursor->getErrors())
+ )
+ ];
+ }
+
+ /**
+ * Check inventory option available message
+ *
+ * @return bool
+ */
+ private function isStockItemMessageEnabled(): bool
+ {
+ return (int) $this->scopeConfig->getValue('cataloginventory/options/not_available_message') === 1;
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/ValidateProductCartResolver.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ValidateProductCartResolver.php
new file mode 100644
index 000000000000..de3ec7d27487
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/Cart/ValidateProductCartResolver.php
@@ -0,0 +1,41 @@
+getCartForUser = $getCartForUser;
- $this->addProductsToCartService = $addProductsToCart;
- $this->quoteMutex = $quoteMutex;
- $this->cartItemPrecursor = $cartItemPrecursor ?: ObjectManager::getInstance()->get(PrecursorInterface::class);
}
/**
@@ -76,14 +40,7 @@ public function __construct(
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
{
- if (empty($args['cartId'])) {
- throw new GraphQlInputException(__('Required parameter "cartId" is missing'));
- }
- if (empty($args['cartItems']) || !is_array($args['cartItems'])
- ) {
- throw new GraphQlInputException(__('Required parameter "cartItems" is missing'));
- }
-
+ $this->validateCartResolver->execute($args);
return $this->quoteMutex->execute(
[$args['cartId']],
\Closure::fromCallable([$this, 'run']),
@@ -102,35 +59,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
*/
private function run($context, ?array $args): array
{
- $maskedCartId = $args['cartId'];
- $cartItemsData = $args['cartItems'];
- $storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
-
- // Shopping Cart validation
- $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId);
- $cartItemsData = $this->cartItemPrecursor->process($cartItemsData, $context);
- $cartItems = [];
- foreach ($cartItemsData as $cartItemData) {
- $cartItems[] = (new CartItemFactory())->create($cartItemData);
- }
-
- /** @var AddProductsToCartOutput $addProductsToCartOutput */
- $addProductsToCartOutput = $this->addProductsToCartService->execute($maskedCartId, $cartItems);
-
- return [
- 'cart' => [
- 'model' => $addProductsToCartOutput->getCart(),
- ],
- 'user_errors' => array_map(
- function (Error $error) {
- return [
- 'code' => $error->getCode(),
- 'message' => $error->getMessage(),
- 'path' => [$error->getCartItemPosition()]
- ];
- },
- array_merge($addProductsToCartOutput->getErrors(), $this->cartItemPrecursor->getErrors())
- )
- ];
+ return $this->addProductsToCartService->execute($context, $args);
}
}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartErrorTypeResolver.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartErrorTypeResolver.php
new file mode 100644
index 000000000000..9e8cded85dfe
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartErrorTypeResolver.php
@@ -0,0 +1,45 @@
+ 0) {
+ return "InsufficientStockError";
+ }
+
+ return $errorType;
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
index 8764b4c2c2d2..68ad5935af17 100644
--- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
@@ -1,5 +1,5 @@
-# Copyright © Magento, Inc. All rights reserved.
-# See COPYING.txt for license details.
+# Copyright 2024 Adobe
+# All Rights Reserved.
type Query {
"""phpcs:ignore Magento2.GraphQL.ValidArgumentName"""
@@ -487,14 +487,21 @@ type Order @doc(description: "Contains the order ID.") {
order_id: String @deprecated(reason: "Use `order_number` instead.")
}
-type CartUserInputError @doc(description:"An error encountered while adding an item to the the cart.") {
+interface Error @typeResolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartErrorTypeResolver") @doc(description:"An error encountered while adding an item to the the cart.") {
message: String! @doc(description: "A localized error message.")
code: CartUserInputErrorType! @doc(description: "A cart-specific error code.")
}
+type CartUserInputError implements Error {
+}
+
+type InsufficientStockError implements Error {
+ quantity: Float @doc(description: "Amount of available stock")
+}
+
type AddProductsToCartOutput @doc(description: "Contains details about the cart after adding products to it.") {
cart: Cart! @doc(description: "The cart after products have been added.")
- user_errors: [CartUserInputError!]! @doc(description: "Contains errors encountered while adding an item to the cart.")
+ user_errors: [Error!]! @doc(description: "Contains errors encountered while adding an item to the cart.")
}
enum CartUserInputErrorType {
diff --git a/app/code/Magento/Sales/Model/InsertOrderStatusChangeHistory.php b/app/code/Magento/Sales/Model/InsertOrderStatusChangeHistory.php
new file mode 100644
index 000000000000..8a3b03b9e3fd
--- /dev/null
+++ b/app/code/Magento/Sales/Model/InsertOrderStatusChangeHistory.php
@@ -0,0 +1,38 @@
+salesOrderStatusChangeHistoryResourceModel->getLatestStatus((int)$order->getId());
+ if ((!$latestStatus && $order->getStatus()) ||
+ (isset($latestStatus['status']) && $latestStatus['status'] !== $order->getStatus())
+ ) {
+ $this->salesOrderStatusChangeHistoryResourceModel->insert($order);
+ }
+ }
+}
diff --git a/app/code/Magento/Sales/Model/ResourceModel/SalesOrderStatusChangeHistory.php b/app/code/Magento/Sales/Model/ResourceModel/SalesOrderStatusChangeHistory.php
new file mode 100644
index 000000000000..821de316be74
--- /dev/null
+++ b/app/code/Magento/Sales/Model/ResourceModel/SalesOrderStatusChangeHistory.php
@@ -0,0 +1,101 @@
+resourceConnection->getConnection();
+ return $connection->fetchRow(
+ $connection->select()->from(
+ $connection->getTableName(self::TABLE_NAME),
+ ['status', 'created_at']
+ )->where(
+ 'order_id = ?',
+ $orderId
+ )->order('created_at DESC')
+ ) ?: null;
+ }
+
+ /**
+ * Insert updated status against an order into the table
+ *
+ * @param Order $order
+ * @return void
+ */
+ public function insert(Order $order): void
+ {
+ if (!$this->isOrderExists((int)$order->getId()) || $order->getStatus() === null) {
+ return;
+ }
+
+ $connection = $this->resourceConnection->getConnection();
+ $connection->insert(
+ $connection->getTableName(self::TABLE_NAME),
+ [
+ 'order_id' => (int)$order->getId(),
+ 'status' => $order->getStatus()
+ ]
+ );
+ }
+
+ /**
+ * Check if order exists in db or is deleted
+ *
+ * @param int $orderId
+ * @return bool
+ */
+ private function isOrderExists(int $orderId): bool
+ {
+ $connection = $this->resourceConnection->getConnection();
+ $entityId = $connection->fetchOne(
+ $connection->select()->from(
+ $connection->getTableName(self::ORDER_TABLE_NAME),
+ ['entity_id']
+ )->where(
+ 'entity_id = ?',
+ $orderId
+ )
+ );
+ return (int) $entityId === $orderId;
+ }
+}
diff --git a/app/code/Magento/Sales/Observer/StoreStatusChangeObserver.php b/app/code/Magento/Sales/Observer/StoreStatusChangeObserver.php
new file mode 100644
index 000000000000..984f4b7523cd
--- /dev/null
+++ b/app/code/Magento/Sales/Observer/StoreStatusChangeObserver.php
@@ -0,0 +1,45 @@
+getEvent()->getOrder();
+
+ if (!$order->getId()) {
+ //order not saved in the database
+ return $this;
+ }
+
+ //Insert order status into sales_order_status_change_history table if the order status is changed
+ $this->salesOrderStatusChangeHistory->execute($order);
+ return $this;
+ }
+}
diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml
index c01354500eb7..cc48024217d0 100644
--- a/app/code/Magento/Sales/etc/db_schema.xml
+++ b/app/code/Magento/Sales/etc/db_schema.xml
@@ -1,8 +1,19 @@
+
diff --git a/app/code/Magento/Sales/etc/db_schema_whitelist.json b/app/code/Magento/Sales/etc/db_schema_whitelist.json
index 664c65d36c3c..6670f1e3866a 100644
--- a/app/code/Magento/Sales/etc/db_schema_whitelist.json
+++ b/app/code/Magento/Sales/etc/db_schema_whitelist.json
@@ -1246,5 +1246,18 @@
"SALES_ORDER_STATUS_LABEL_STATUS_SALES_ORDER_STATUS_STATUS": true,
"SALES_ORDER_STATUS_LABEL_STORE_ID_STORE_STORE_ID": true
}
+ },
+ "sales_order_status_change_history": {
+ "column": {
+ "entity_id": true,
+ "order_id": true,
+ "status": true,
+ "created_at": true,
+ "updated_at": true
+ },
+ "constraint": {
+ "PRIMARY": true,
+ "SALES_ORDER_STATUS_CHANGE_HISTORY_ORDER_ID_SALES_ORDER_ENTITY_ID": true
+ }
}
}
diff --git a/app/code/Magento/Sales/etc/events.xml b/app/code/Magento/Sales/etc/events.xml
index b3a7a4ab9957..17a411ddaf65 100644
--- a/app/code/Magento/Sales/etc/events.xml
+++ b/app/code/Magento/Sales/etc/events.xml
@@ -1,8 +1,19 @@
@@ -56,4 +67,7 @@
name="sales_assign_order_to_customer"
instance="Magento\Sales\Observer\AssignOrderToCustomerObserver" />
+
+
+
diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderStatusChangeDate.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderStatusChangeDate.php
new file mode 100644
index 000000000000..eba8c4ec9f86
--- /dev/null
+++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderStatusChangeDate.php
@@ -0,0 +1,48 @@
+salesOrderStatusChangeHistory->getLatestStatus((int)$order->getId());
+ return ($latestStatus)
+ ? $this->localeDate->convertConfigTimeToUtc($latestStatus['created_at'], DateTime::DATE_PHP_FORMAT)
+ : '';
+ }
+}
diff --git a/app/code/Magento/SalesGraphQl/etc/graphql/di.xml b/app/code/Magento/SalesGraphQl/etc/graphql/di.xml
index 7969587eb659..155ba2eca0f3 100644
--- a/app/code/Magento/SalesGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/SalesGraphQl/etc/graphql/di.xml
@@ -1,8 +1,8 @@
@@ -61,4 +61,25 @@
+
+
+
+ - tax/display/type
+ - tax/display/shipping
+ - tax/sales_display/price
+ - tax/sales_display/subtotal
+ - tax/sales_display/shipping
+ - tax/sales_display/grandtotal
+ - tax/sales_display/full_summary
+ - tax/sales_display/zero_tax
+ - tax/weee/enable
+ - tax/weee/display_list
+ - tax/weee/display
+ - tax/weee/display_sales
+ - tax/weee/display_email
+ - tax/weee/apply_vat
+ - tax/weee/include_in_subtotal
+
+
+
diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls
index e1559dc18aee..4e291a0a2623 100644
--- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls
@@ -1,5 +1,5 @@
-# Copyright © Magento, Inc. All rights reserved.
-# See COPYING.txt for license details.
+# Copyright 2024 Adobe
+# All Rights Reserved.
type Query {
customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @deprecated(reason: "Use the `customer` query instead.") @cache(cacheable: false)
@@ -82,6 +82,7 @@ type CustomerOrder @doc(description: "Contains details about each of the custome
is_virtual: Boolean! @doc(description: "`TRUE` if the order is virtual") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderIsVirtual")
available_actions: [OrderActionType!]! @doc(description: "List of available order actions.") @resolver(class: "\\Magento\\SalesGraphQl\\Model\\Resolver\\OrderAvailableActions")
customer_info: OrderCustomerInfo! @doc(description: "Returns customer information from order.") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderCustomerInfo")
+ order_status_change_date: String! @doc(description: "The date the order status was last updated.") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderStatusChangeDate")
}
type OrderCustomerInfo {
@@ -315,3 +316,21 @@ input OrderInformationInput @doc(description: "Input to retrieve an order based
enum OrderActionType @doc(description: "The list of available order actions.") {
REORDER
}
+
+type StoreConfig {
+ display_product_prices_in_catalog: Boolean! @doc(description: "Configuration data from tax/display/type")
+ display_shipping_prices: Boolean! @doc(description: "Configuration data from tax/display/shipping")
+ orders_invoices_credit_memos_display_price: Boolean! @doc(description: "Configuration data from tax/sales_display/price")
+ orders_invoices_credit_memos_display_subtotal: Boolean! @doc(description: "Configuration data from tax/sales_display/subtotal")
+ orders_invoices_credit_memos_display_shipping_amount: Boolean! @doc(description: "Configuration data from tax/sales_display/shipping")
+ orders_invoices_credit_memos_display_grandtotal: Boolean! @doc(description: "Configuration data from tax/sales_display/grandtotal")
+ orders_invoices_credit_memos_display_full_summary: Boolean! @doc(description: "Configuration data from tax/sales_display/full_summary")
+ orders_invoices_credit_memos_display_zero_tax: Boolean! @doc(description: "Configuration data from tax/sales_display/zero_tax")
+ fixed_product_taxes_enable: Boolean! @doc(description: "Configuration data from tax/weee/enable")
+ fixed_product_taxes_display_prices_in_product_lists: Int @doc(description: "Configuration data from tax/weee/display_list")
+ fixed_product_taxes_display_prices_on_product_view_page: Int @doc(description: "Configuration data from tax/weee/display")
+ fixed_product_taxes_display_prices_in_sales_modules: Int @doc(description: "Configuration data from tax/weee/display_sales")
+ fixed_product_taxes_display_prices_in_emails: Int @doc(description: "Configuration data from tax/weee/display_email")
+ fixed_product_taxes_apply_tax_to_fpt: Boolean! @doc(description: "Configuration data from tax/weee/apply_vat")
+ fixed_product_taxes_include_fpt_in_subtotal: Boolean! @doc(description: "Configuration data from tax/weee/include_in_subtotal")
+}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerAddressesV2Test.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerAddressesV2Test.php
new file mode 100644
index 000000000000..8a4200be746d
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerAddressesV2Test.php
@@ -0,0 +1,213 @@
+ 'customer@example.com',], 'customer')
+]
+class GetCustomerAddressesV2Test extends GraphQlAbstract
+{
+ /**
+ * @var CustomerTokenServiceInterface
+ */
+ private $customerTokenService;
+
+ /**
+ * Initialize fixture namespaces.
+ *
+ * @return void
+ */
+ protected function setUp(): void
+ {
+ $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class);
+ }
+
+ /**
+ * @param int $pageSize
+ * @param int $currentPage
+ * @param array $expectedResponse
+ * @return void
+ * @throws AuthenticationException
+ * @dataProvider dataProviderGetCustomerAddressesV2
+ */
+ public function testGetCustomerAddressesV2(int $pageSize, int $currentPage, array $expectedResponse)
+ {
+ $query = $this->getQuery($pageSize, $currentPage);
+ $response = $this->graphQlQuery(
+ $query,
+ [],
+ '',
+ $this->getCustomerAuthHeaders('customer@example.com', 'password')
+ );
+ self::assertArrayHasKey('addressesV2', $response['customer']);
+ $addressesV2 = $response['customer']['addressesV2'];
+ self::assertNotEmpty($addressesV2);
+ self::assertIsArray($addressesV2);
+ self::assertEquals($expectedResponse['items_count'], count($addressesV2['items']));
+ self::assertEquals($expectedResponse['total_count'], $addressesV2['total_count']);
+ self::assertEquals($expectedResponse['page_info'], $addressesV2['page_info']);
+ }
+
+ public function testAddressesV2NotAuthorized()
+ {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('The current customer isn\'t authorized.');
+ $query = $this->getQuery();
+ $this->graphQlQuery($query);
+ }
+
+ /**
+ * @throws AuthenticationException
+ * @throws Exception
+ */
+ #[
+ DataFixture(Customer::class, ['email' => 'customer2@example.com',], 'customer2')
+ ]
+ public function testAddressesV2ForCustomerWithoutAddresses()
+ {
+ $query = $this->getQuery();
+ $response = $this->graphQlQuery(
+ $query,
+ [],
+ '',
+ $this->getCustomerAuthHeaders('customer2@example.com', 'password')
+ );
+ $addressesV2 = $response['customer']['addressesV2'];
+ $this->assertEmpty($addressesV2['items']);
+ $this->assertEquals(0, $addressesV2['total_count']);
+ $this->assertEquals(0, $addressesV2['page_info']['total_pages']);
+ }
+
+ /**
+ * Data provider for customer address input
+ *
+ * @return array
+ */
+ public static function dataProviderGetCustomerAddressesV2(): array
+ {
+ return [
+ 'scenario_1' => [
+ 'pageSize' => 1,
+ 'currentPage' => 1,
+ 'expectedResponse' => [
+ 'items_count' => 1,
+ 'page_info' => [
+ 'page_size' => 1,
+ 'current_page' => 1,
+ 'total_pages' => 3
+ ],
+ 'total_count' => 3
+ ]
+ ],
+ 'scenario_2' => [
+ 'pageSize' => 2,
+ 'currentPage' => 1,
+ 'expectedResponse' => [
+ 'items_count' => 2,
+ 'page_info' => [
+ 'page_size' => 2,
+ 'current_page' => 1,
+ 'total_pages' => 2
+ ],
+ 'total_count' => 3
+ ]
+ ],
+ 'scenario_3' => [
+ 'pageSize' => 2,
+ 'currentPage' => 2,
+ 'expectedResponse' => [
+ 'items_count' => 1,
+ 'page_info' => [
+ 'page_size' => 2,
+ 'current_page' => 2,
+ 'total_pages' => 2
+ ],
+ 'total_count' => 3
+ ]
+ ],
+ 'scenario_4' => [
+ 'pageSize' => 3,
+ 'currentPage' => 1,
+ 'expectedResponse' => [
+ 'items_count' => 3,
+ 'page_info' => [
+ 'page_size' => 3,
+ 'current_page' => 1,
+ 'total_pages' => 1
+ ],
+ 'total_count' => 3
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * Get customer auth headers
+ *
+ * @param string $email
+ * @param string $password
+ * @return string[]
+ * @throws AuthenticationException
+ */
+ private function getCustomerAuthHeaders(string $email, string $password): array
+ {
+ $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password);
+ return ['Authorization' => 'Bearer ' . $customerToken];
+ }
+
+ /**
+ * Get addressesV2 query
+ *
+ * @param int $pageSize
+ * @param int $currentPage
+ * @return string
+ */
+ private function getQuery(int $pageSize = 5, int $currentPage = 1): string
+ {
+ return <<graphQlMutation($query);
}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/CancelGuestOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/CancelGuestOrderTest.php
index bb8ece5d1e95..c63c989ee744 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/CancelGuestOrderTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/CancelGuestOrderTest.php
@@ -18,7 +18,8 @@
use Exception;
use Magento\Checkout\Test\Fixture\SetGuestEmail as SetGuestEmailFixture;
-use Magento\Framework\ObjectManagerInterface;
+use Magento\Customer\Test\Fixture\Customer;
+use Magento\Quote\Test\Fixture\CustomerCart;
use Magento\Quote\Test\Fixture\GuestCart;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
@@ -43,6 +44,7 @@
use Magento\Checkout\Test\Fixture\SetPaymentMethod as SetPaymentMethodFixture;
use Magento\Checkout\Test\Fixture\SetShippingAddress as SetShippingAddressFixture;
use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture;
+use Magento\SalesGraphQl\Model\Order\Token;
/**
* Test coverage for cancel order mutation for guest order
@@ -66,29 +68,41 @@
class CancelGuestOrderTest extends GraphQlAbstract
{
/**
- * @var ObjectManagerInterface
- */
- private $objectManager;
-
- /**
- * @inheritdoc
+ * @return void
+ * @throws Exception
*/
- protected function setUp(): void
+ public function testAttemptToCancelOrderWhenMissingToken()
{
- $this->objectManager = Bootstrap::getObjectManager();
+ $query = <<expectException(ResponseContainsErrorsException::class);
+ $this->expectExceptionMessage('Field GuestOrderCancelInput.token of required type String! was not provided.');
+ $this->graphQlMutation($query);
}
/**
* @return void
* @throws Exception
*/
- public function testAttemptToCancelOrderWhenMissingOrderId()
+ public function testAttemptToCancelOrderWhenMissingReason()
{
$query = <<expectException(ResponseContainsErrorsException::class);
- $this->expectExceptionMessage("Field CancelOrderInput.order_id of required type ID! was not provided.");
+ $this->expectExceptionMessage("Field GuestOrderCancelInput.reason of required type String! was not provided.");
$this->graphQlMutation($query);
}
-
/**
* @return void
* @throws Exception
*/
- public function testAttemptToCancelOrderWhenMissingReason()
+ public function testAttemptToCancelOrderWithInvalidToken()
{
$query = <<expectException(ResponseContainsErrorsException::class);
- $this->expectExceptionMessage("Field CancelOrderInput.reason of required type String! was not provided.");
+ $this->expectExceptionMessage('We couldn\'t locate an order with the information provided.');
$this->graphQlMutation($query);
}
@@ -132,6 +146,7 @@ public function testAttemptToCancelOrderWhenMissingReason()
* @return void
* @throws AuthenticationException
* @throws LocalizedException
+ * @throws Exception
*/
#[
Config('sales/cancellation/enabled', 0)
@@ -142,27 +157,11 @@ public function testAttemptToCancelOrderWhenCancellationFeatureDisabled()
* @var $order OrderInterface
*/
$order = DataFixtureStorageManager::getStorage()->get('order');
+ $query = $this->getMutation($order);
- $query = <<getEntityId()},
- reason: "Sample reason"
- }
- ){
- errorV2 {
- message
- }
- order {
- status
- }
- }
- }
-MUTATION;
$this->assertEquals(
[
- 'cancelOrder' => [
+ 'requestGuestOrderCancel' => [
'errorV2' => [
'message' => 'Order cancellation is not enabled for requested store.'
],
@@ -179,6 +178,7 @@ public function testAttemptToCancelOrderWhenCancellationFeatureDisabled()
* @return void
* @throws AuthenticationException
* @throws LocalizedException
+ * @throws Exception
*
* @dataProvider orderStatusProvider
*/
@@ -195,14 +195,13 @@ public function testAttemptToCancelOrderWithSomeStatuses(string $status, string
$order->setState($status);
/** @var OrderRepositoryInterface $orderRepo */
- $orderRepo = $this->objectManager->get(OrderRepository::class);
+ $orderRepo = Bootstrap::getObjectManager()->create(OrderRepository::class);
$orderRepo->save($order);
- $query = $this->getCancelOrderMutation($order);
+ $query = $this->getMutation($order);
$this->assertEquals(
[
- 'cancelOrder' =>
- [
+ 'requestGuestOrderCancel' => [
'errorV2' => [
'message' => 'Order already closed, complete, cancelled or on hold'
],
@@ -219,6 +218,7 @@ public function testAttemptToCancelOrderWithSomeStatuses(string $status, string
* @return void
* @throws AuthenticationException
* @throws LocalizedException
+ * @throws Exception
*/
#[
DataFixture(Store::class),
@@ -241,11 +241,10 @@ public function testAttemptToCancelOrderWithOfflinePaymentFullyInvoicedFullyShip
* @var $order OrderInterface
*/
$order = DataFixtureStorageManager::getStorage()->get('order');
- $query = $this->getCancelOrderMutation($order);
+ $query = $this->getMutation($order);
$this->assertEquals(
[
- 'cancelOrder' =>
- [
+ 'requestGuestOrderCancel' => [
'errorV2' => [
'message' => 'Order already closed, complete, cancelled or on hold'
],
@@ -262,6 +261,7 @@ public function testAttemptToCancelOrderWithOfflinePaymentFullyInvoicedFullyShip
* @return void
* @throws AuthenticationException
* @throws LocalizedException
+ * @throws Exception
*/
#[
DataFixture(Store::class),
@@ -297,11 +297,10 @@ public function testAttemptToCancelOrderWithOfflinePaymentFullyInvoicedPartially
* @var $order OrderInterface
*/
$order = DataFixtureStorageManager::getStorage()->get('order');
- $query = $this->getCancelOrderMutation($order);
+ $query = $this->getMutation($order);
$this->assertEquals(
[
- 'cancelOrder' =>
- [
+ 'requestGuestOrderCancel' => [
'errorV2' => [
'message' => 'Order with one or more items shipped cannot be cancelled'
],
@@ -318,6 +317,7 @@ public function testAttemptToCancelOrderWithOfflinePaymentFullyInvoicedPartially
* @return void
* @throws AuthenticationException
* @throws LocalizedException
+ * @throws Exception
*/
#[
DataFixture(Store::class),
@@ -340,11 +340,10 @@ public function testAttemptToCancelOrderWithOfflinePaymentFullyInvoicedFullyRefu
* @var $order OrderInterface
*/
$order = DataFixtureStorageManager::getStorage()->get('order');
- $query = $this->getCancelOrderMutation($order);
+ $query = $this->getMutation($order);
$this->assertEquals(
[
- 'cancelOrder' =>
- [
+ 'requestGuestOrderCancel' => [
'errorV2' => [
'message' => 'Order already closed, complete, cancelled or on hold'
],
@@ -357,6 +356,9 @@ public function testAttemptToCancelOrderWithOfflinePaymentFullyInvoicedFullyRefu
);
}
+ /**
+ * @throws LocalizedException
+ */
#[
DataFixture(Store::class),
DataFixture(ProductFixture::class, as: 'product'),
@@ -376,11 +378,10 @@ public function testCancelOrderWithOutAnyAmountPaid()
* @var $order OrderInterface
*/
$order = DataFixtureStorageManager::getStorage()->get('order');
- $query = $this->getCancelOrderMutation($order);
+ $query = $this->getMutation($order);
$this->assertEquals(
[
- 'cancelOrder' =>
- [
+ 'requestGuestOrderCancel' => [
'errorV2' => null,
'order' => [
'status' => 'Pending'
@@ -396,19 +397,61 @@ public function testCancelOrderWithOutAnyAmountPaid()
}
/**
- * Get cancel order mutation
+ * @throws AuthenticationException
+ * @throws LocalizedException
+ * @throws Exception
+ */
+ #[
+ DataFixture(Store::class),
+ DataFixture(
+ Customer::class,
+ [
+ 'email' => 'customer@example.com',
+ 'password' => 'password'
+ ],
+ 'customer'
+ ),
+ DataFixture(ProductFixture::class, as: 'product'),
+ DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], as: 'cart'),
+ DataFixture(
+ AddProductToCartFixture::class,
+ [
+ 'cart_id' => '$cart.id$',
+ 'product_id' => '$product.id$',
+ 'qty' => 3
+ ]
+ ),
+ 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'),
+ Config('sales/cancellation/enabled', 1)
+ ]
+ public function testOrderCancellationForLoggedInCustomerUsingToken()
+ {
+ $this->expectException(ResponseContainsErrorsException::class);
+ $this->expectExceptionMessage('Please login to view the order.');
+
+ /** @var OrderInterface $order */
+ $order = DataFixtureStorageManager::getStorage()->get('order');
+ $this->graphQlMutation($this->getMutation($order));
+ }
+
+ /**
+ * Get request guest order cancellation by token mutation
*
- * @param Order $order
+ * @param OrderInterface $order
* @return string
*/
- private function getCancelOrderMutation(OrderInterface $order): string
+ private function getMutation(OrderInterface $order): string
{
return <<getEntityId()}"
- reason: "Cancel sample reason"
+ token: "{$this->getOrderToken($order)}",
+ reason: "Sample reason"
}
){
errorV2 {
@@ -422,10 +465,25 @@ private function getCancelOrderMutation(OrderInterface $order): string
MUTATION;
}
+ /**
+ * Get token from order
+ *
+ * @param OrderInterface $order
+ * @return string
+ */
+ private function getOrderToken(OrderInterface $order): string
+ {
+ return Bootstrap::getObjectManager()->create(Token::class)->encrypt(
+ $order->getIncrementId(),
+ $order->getBillingAddress()->getEmail(),
+ $order->getBillingAddress()->getPostcode()
+ );
+ }
+
/**
* @return array[]
*/
- public static function orderStatusProvider(): array
+ public function orderStatusProvider(): array
{
return [
'On Hold status' => [
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/ConfirmCancelGuestOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/ConfirmCancelGuestOrderTest.php
index e13ea7fed85c..aeafa931715e 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/ConfirmCancelGuestOrderTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/ConfirmCancelGuestOrderTest.php
@@ -126,9 +126,16 @@ public function testAttemptToConfirmCancelNonExistingOrder()
}
}
MUTATION;
- $this->expectException(ResponseContainsErrorsException::class);
- $this->expectExceptionMessage("The entity that was requested doesn't exist. Verify the entity and try again.");
- $this->graphQlMutation($query);
+
+ $this->assertEquals(
+ [
+ 'confirmCancelOrder' => [
+ 'error' => 'The entity that was requested doesn\'t exist. Verify the entity and try again.',
+ 'order' => null
+ ]
+ ],
+ $this->graphQlMutation($query)
+ );
}
/**
@@ -349,9 +356,18 @@ public function testAttemptToConfirmCancelOrderForWhichConfirmationKeyNotGenerat
*/
$order = DataFixtureStorageManager::getStorage()->get('order');
$query = $this->getConfirmCancelOrderMutation($order);
- $this->expectException(ResponseContainsErrorsException::class);
- $this->expectExceptionMessage("The order cancellation could not be confirmed.");
- $this->graphQlMutation($query);
+ $this->assertEquals(
+ [
+ 'confirmCancelOrder' =>
+ [
+ 'errorV2' => [
+ 'message' => 'The order cancellation could not be confirmed.'
+ ],
+ 'order' => null
+ ]
+ ],
+ $this->graphQlMutation($query)
+ );
}
#[
@@ -410,9 +426,18 @@ public function testAttemptToConfirmCancelOrderWithInvalidConfirmationKey()
$this->confirmationKey->execute($order, 'Simple reason');
$query = $this->getConfirmCancelOrderMutation($order);
- $this->expectException(ResponseContainsErrorsException::class);
- $this->expectExceptionMessage("The order cancellation could not be confirmed.");
- $this->graphQlMutation($query);
+ $this->assertEquals(
+ [
+ 'confirmCancelOrder' =>
+ [
+ 'errorV2' => [
+ 'message' => 'The order cancellation could not be confirmed.'
+ ],
+ 'order' => null
+ ]
+ ],
+ $this->graphQlMutation($query)
+ );
}
#[
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/InsufficientStockErrorTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/InsufficientStockErrorTest.php
new file mode 100644
index 000000000000..2a4f9e5fdabe
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/InsufficientStockErrorTest.php
@@ -0,0 +1,123 @@
+ self::SKU, 'price' => 100.00], 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 testInsufficientStockError(): void
+ {
+ $maskedQuoteId = DataFixtureStorageManager::getStorage()->get('quoteIdMask')->getMaskedId();
+ $query = $this->mutationAddProduct($maskedQuoteId, self::SKU, 200);
+ $response = $this->graphQlMutation($query);
+ $responseDataObject = new DataObject($response);
+
+ $this->assertEquals(
+ $responseDataObject->getData('addProductsToCart/user_errors/0/__typename'),
+ 'InsufficientStockError'
+ );
+
+ $this->assertEquals(
+ $responseDataObject->getData('addProductsToCart/user_errors/0/quantity'),
+ 100
+ );
+ }
+
+ #[
+ Config('cataloginventory/options/not_available_message', 0),
+ DataFixture(ProductFixture::class, ['sku' => self::SKU, 'price' => 100.00], 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 testCartUserInputError(): void
+ {
+ $maskedQuoteId = DataFixtureStorageManager::getStorage()->get('quoteIdMask')->getMaskedId();
+ $query = $this->mutationAddProduct($maskedQuoteId, self::SKU, 200);
+ $response = $this->graphQlMutation($query);
+ $responseDataObject = new DataObject($response);
+
+ $this->assertEquals(
+ $responseDataObject->getData('addProductsToCart/user_errors/0/__typename'),
+ 'CartUserInputError'
+ );
+
+ $this->assertArrayNotHasKey(
+ 'quantity',
+ $responseDataObject->getData('addProductsToCart/user_errors')
+ );
+ }
+
+ private function mutationAddProduct(string $cartId, string $sku, int $qty = 1): string
+ {
+ return << '$cart.id$', 'product_id' => '$product.id$']),
+ DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']),
+ DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']),
+ DataFixture(SetGuestEmailFixture::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'),
+]
+class OrderStatusChangeDateTest extends GraphQlAbstract
+{
+ /**
+ * Order status mapper
+ */
+ private const STATUS_MAPPER = [
+ Order::STATE_HOLDED => 'On Hold',
+ Order::STATE_CANCELED => 'Canceled'
+ ];
+
+ public function testOrderStatusChangeDateWithStatusChange(): void
+ {
+ /**
+ * @var $order OrderInterface
+ */
+ $order = DataFixtureStorageManager::getStorage()->get('order');
+
+ $this->assertOrderStatusChangeDate($order, Order::STATE_HOLDED);
+ $this->assertOrderStatusChangeDate($order, Order::STATE_CANCELED);
+ }
+
+ /**
+ * Assert order_status_change_date after setting the status
+ *
+ * @param OrderInterface $order
+ * @param string $status
+ * @return void
+ */
+ private function assertOrderStatusChangeDate(OrderInterface $order, string $status): void
+ {
+ $orderRepo = Bootstrap::getObjectManager()->get(OrderRepository::class);
+ $timeZone = Bootstrap::getObjectManager()->get(TimezoneInterface::class);
+
+ //Update order status
+ $order->setStatus($status);
+ $order->setState($status);
+ $orderRepo->save($order);
+
+ $updatedGuestOrder = $this->graphQlMutation($this->getQuery(
+ $order->getIncrementId(),
+ $order->getBillingAddress()->getEmail(),
+ $order->getBillingAddress()->getPostcode()
+ ));
+ self::assertEquals(
+ self::STATUS_MAPPER[$status],
+ $updatedGuestOrder['guestOrder']['status']
+ );
+ self::assertEquals(
+ $timeZone->convertConfigTimeToUtc($order->getCreatedAt(), DateTime::DATE_PHP_FORMAT),
+ $updatedGuestOrder['guestOrder']['order_status_change_date']
+ );
+ }
+
+ /**
+ * Generates guestOrder query with order_status_change_date
+ *
+ * @param string $number
+ * @param string $email
+ * @param string $postcode
+ * @return string
+ */
+ private function getQuery(string $number, string $email, string $postcode): string
+ {
+ return <<graphQlQuery($this->getQuery());
+ $this->assertArrayHasKey('storeConfig', $response);
+ $this->assertStoreConfigsExist($response['storeConfig']);
+ }
+
+ /**
+ * Check if all the added store configs are returned in graphql response
+ *
+ * @param array $response
+ * @return void
+ */
+ private function assertStoreConfigsExist(array $response): void
+ {
+ foreach (self::CONFIG_KEYS as $key) {
+ $this->assertArrayHasKey($key, $response);
+ }
+ }
+
+ /**
+ * Generates storeConfig query with newly added configurations from sales->tax
+ *
+ * @return string
+ */
+ private function getQuery(): string
+ {
+ return <<