diff --git a/vendor/magento/module-graph-ql/Helper/Error/AggregateExceptionMessageFormatter.php b/vendor/magento/module-graph-ql/Helper/Error/AggregateExceptionMessageFormatter.php index 1eab96a3f229d..5cdb66a6e519b 100644 --- a/vendor/magento/module-graph-ql/Helper/Error/AggregateExceptionMessageFormatter.php +++ b/vendor/magento/module-graph-ql/Helper/Error/AggregateExceptionMessageFormatter.php @@ -1,7 +1,7 @@ messageFormatters as $formatter) { - $formatted = $formatter->getFormatted($e, $messagePrefix, $field, $context, $info); - if ($formatted) { + if ($formatted = $formatter->getFormatted($e, $messagePrefix, $field, $context, $info)) { return $formatted; } } - return new GraphQlInputException($defaultMessage, $e); + + $messageText = $e->getCode() ? __($e->getMessage()) : $defaultMessage; + $messageWithPrefix = empty($messagePrefix) + ? $messageText + : __("$messagePrefix: %message", ['message' => $messageText]); + + return new GraphQlInputException($messageWithPrefix, $e, $e->getCode()); } } diff --git a/vendor/magento/module-quote-graph-ql/Model/Cart/GetCartForCheckout.php b/vendor/magento/module-quote-graph-ql/Model/Cart/GetCartForCheckout.php index 4b2d1afdea003..bd5f3d8cbaf2e 100644 --- a/vendor/magento/module-quote-graph-ql/Model/Cart/GetCartForCheckout.php +++ b/vendor/magento/module-quote-graph-ql/Model/Cart/GetCartForCheckout.php @@ -1,7 +1,7 @@ getCartForUser->execute($cartHash, $customerId, $storeId); } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e, $e->getCode()); } $this->checkoutAllowance->execute($cart); diff --git a/vendor/magento/module-quote-graph-ql/Model/Cart/GetCartForUser.php b/vendor/magento/module-quote-graph-ql/Model/Cart/GetCartForUser.php index 77a31cc3cd023..72b4da1188d3d 100644 --- a/vendor/magento/module-quote-graph-ql/Model/Cart/GetCartForUser.php +++ b/vendor/magento/module-quote-graph-ql/Model/Cart/GetCartForUser.php @@ -1,7 +1,7 @@ maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; $this->cartRepository = $cartRepository; $this->isActive = $isActive; $this->updateCartCurrency = $updateCartCurrency; + $this->errorMapper = $errorMapper; } /** @@ -78,12 +87,18 @@ public function execute(string $cartHash, ?int $customerId, int $storeId): Quote $cart = $this->cartRepository->get($cartId); } catch (NoSuchEntityException $exception) { throw new GraphQlNoSuchEntityException( - __('Could not find a cart with ID "%masked_cart_id"', ['masked_cart_id' => $cartHash]) + __('Could not find a cart with ID "%masked_cart_id"', ['masked_cart_id' => $cartHash]), + $exception, + $this->errorMapper->getErrorMessageId('Could not find a cart with ID') ); } if (false === (bool)$this->isActive->execute($cart)) { - throw new GraphQlNoSuchEntityException(__('The cart isn\'t active.')); + throw new GraphQlNoSuchEntityException( + __('The cart isn\'t active.'), + null, + $this->errorMapper->getErrorMessageId('The cart isn\'t active') + ); } $cart = $this->updateCartCurrency->execute($cart, $storeId); diff --git a/vendor/magento/module-quote-graph-ql/Model/ErrorMapper.php b/vendor/magento/module-quote-graph-ql/Model/ErrorMapper.php new file mode 100644 index 0000000000000..92f3159e9f727 --- /dev/null +++ b/vendor/magento/module-quote-graph-ql/Model/ErrorMapper.php @@ -0,0 +1,102 @@ + self::ERROR_CART_NOT_FOUND_ID, + 'The cart isn\'t active' => self::ERROR_CART_NOT_ACTIVE_ID, + 'Guest email for cart is missing' => self::ERROR_GUEST_EMAIL_MISSING_ID, + 'A server error stopped your order from being placed. Please try to place your order again' => + self::ERROR_UNABLE_TO_PLACE_ORDER_ID, + 'Some addresses can\'t be used due to the configurations for specific countries' => + self::ERROR_UNABLE_TO_PLACE_ORDER_ID, + 'The shipping method is missing. Select the shipping method and try again' => + self::ERROR_UNABLE_TO_PLACE_ORDER_ID, + 'Please check the billing address information' => self::ERROR_UNABLE_TO_PLACE_ORDER_ID, + 'Enter a valid payment method and try again' => self::ERROR_UNABLE_TO_PLACE_ORDER_ID, + 'Some of the products are out of stock' => self::ERROR_UNABLE_TO_PLACE_ORDER_ID, + 'Unable to place order' => self::ERROR_UNABLE_TO_PLACE_ORDER_ID, + ]; + + /** + * List of error message ids and codes. + */ + public const MESSAGE_CODE_IDS = [ + self::ERROR_CART_NOT_FOUND_ID => self::ERROR_CART_NOT_FOUND, + self::ERROR_CART_NOT_ACTIVE_ID => self::ERROR_CART_NOT_ACTIVE, + self::ERROR_GUEST_EMAIL_MISSING_ID => self::ERROR_GUEST_EMAIL_MISSING, + self::ERROR_UNABLE_TO_PLACE_ORDER_ID => self::ERROR_UNABLE_TO_PLACE_ORDER, + self::ERROR_UNDEFINED_ID => self::ERROR_UNDEFINED + ]; + + /** + * List of error messages and codes. + */ + public const MESSAGE_CODES = [ + 'Could not find a cart with ID' => self::ERROR_CART_NOT_FOUND, + 'The cart isn\'t active' => self::ERROR_CART_NOT_ACTIVE, + 'Guest email for cart is missing' => self::ERROR_GUEST_EMAIL_MISSING, + 'A server error stopped your order from being placed. Please try to place your order again' => + self::ERROR_UNABLE_TO_PLACE_ORDER, + 'Some addresses can\'t be used due to the configurations for specific countries' => + self::ERROR_UNABLE_TO_PLACE_ORDER, + 'The shipping method is missing. Select the shipping method and try again' => + self::ERROR_UNABLE_TO_PLACE_ORDER, + 'Please check the billing address information' => self::ERROR_UNABLE_TO_PLACE_ORDER, + 'Enter a valid payment method and try again' => self::ERROR_UNABLE_TO_PLACE_ORDER, + 'Some of the products are out of stock' => self::ERROR_UNABLE_TO_PLACE_ORDER, + ]; + + /** + * Transforms a message into a corresponding id + * + * @param string $message + * @return int + */ + public function getErrorMessageId(string $message): int + { + $code = self::ERROR_UNDEFINED_ID; + + $matchedCodes = array_filter( + self::MESSAGE_IDS, + function ($key) use ($message) { + return str_contains($message, $key); + }, + ARRAY_FILTER_USE_KEY + ); + + if (!empty($matchedCodes)) { + $code = current($matchedCodes); + } + + return $code; + } +} diff --git a/vendor/magento/module-quote-graph-ql/Model/OrderErrorProcessor.php b/vendor/magento/module-quote-graph-ql/Model/OrderErrorProcessor.php new file mode 100644 index 0000000000000..79b3782f47b16 --- /dev/null +++ b/vendor/magento/module-quote-graph-ql/Model/OrderErrorProcessor.php @@ -0,0 +1,62 @@ +errorMessageFormatter->getFormatted( + $exception, + __('A server error stopped your order from being placed. ' . + 'Please try to place your order again'), + 'Unable to place order', + $field, + $context, + $info + ); + $exceptionCode = $exception->getCode(); + if (!$exceptionCode) { + $exceptionCode = $this->errorMapper->getErrorMessageId($exception->getRawMessage()); + } + + throw new QuoteException(__($exception->getMessage()), $exception, $exceptionCode); + } +} diff --git a/vendor/magento/module-quote-graph-ql/Model/OrderResponseFormatter.php b/vendor/magento/module-quote-graph-ql/Model/OrderResponseFormatter.php new file mode 100644 index 0000000000000..a7ce93b0636a5 --- /dev/null +++ b/vendor/magento/module-quote-graph-ql/Model/OrderResponseFormatter.php @@ -0,0 +1,69 @@ +isApplicable($executionResult)) { + return $executionResult; + } + + $dataErrors = $this->getErrors($executionResult); + if (empty($dataErrors)) { + return $executionResult; + } + + $response = $executionResult['data'][self::NODE_IDENTIFIER] ?: []; + $response['errors'] = $dataErrors; + $executionResult['data'][self::NODE_IDENTIFIER] = $response; + + return $executionResult; + } + + /** + * Check if the formatter is applicable for the given execution result + * + * @param array $executionResult + * @return bool + */ + private function isApplicable(array $executionResult): bool + { + return isset($executionResult['data']) && key_exists(self::NODE_IDENTIFIER, $executionResult['data']); + } + + /** + * Extract errors from the execution result + * + * @param array $executionResult + * @return array + */ + private function getErrors(array $executionResult): array + { + $dataErrors = []; + if (!empty($executionResult['errors'])) { + foreach ($executionResult['errors'] as $error) { + if (isset($error['extensions']['error_code'])) { + $dataErrors[] = [ + 'message' => $error['message'], + 'code' => $error['extensions']['error_code'] + ]; + } + } + } + return $dataErrors; + } +} diff --git a/vendor/magento/module-quote-graph-ql/Model/QuoteException.php b/vendor/magento/module-quote-graph-ql/Model/QuoteException.php new file mode 100644 index 0000000000000..131bca64b8a68 --- /dev/null +++ b/vendor/magento/module-quote-graph-ql/Model/QuoteException.php @@ -0,0 +1,28 @@ +getCategory(); + if ($this->code) { + $extensions['error_code'] = ErrorMapper::MESSAGE_CODE_IDS[$this->code] ?? ErrorMapper::ERROR_UNDEFINED; + } + + return $extensions; + } +} diff --git a/vendor/magento/module-quote-graph-ql/Model/Resolver/EstimateShippingMethods.php b/vendor/magento/module-quote-graph-ql/Model/Resolver/EstimateShippingMethods.php index 69762646d6152..10f9c879264dc 100644 --- a/vendor/magento/module-quote-graph-ql/Model/Resolver/EstimateShippingMethods.php +++ b/vendor/magento/module-quote-graph-ql/Model/Resolver/EstimateShippingMethods.php @@ -21,6 +21,7 @@ use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; use Magento\Quote\Model\Quote\AddressFactory; use Magento\Quote\Model\Cart\ShippingMethodConverter; +use Magento\QuoteGraphQl\Model\ErrorMapper; use Magento\QuoteGraphQl\Model\FormatMoneyTypeData; /** @@ -37,6 +38,7 @@ class EstimateShippingMethods implements ResolverInterface * @param ExtensibleDataObjectConverter $dataObjectConverter * @param ShippingMethodConverter $shippingMethodConverter * @param FormatMoneyTypeData $formatMoneyTypeData + * @param ErrorMapper $errorMapper */ public function __construct( private MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, @@ -46,6 +48,7 @@ public function __construct( private ExtensibleDataObjectConverter $dataObjectConverter, private ShippingMethodConverter $shippingMethodConverter, private FormatMoneyTypeData $formatMoneyTypeData, + private ErrorMapper $errorMapper ) { } @@ -64,7 +67,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, ?array $value [ 'masked_id' => $args['input']['cart_id'] ] - ) + ), + $ex, + $this->errorMapper->getErrorMessageId('Could not find a cart with ID') ); } return $this->getAvailableShippingMethodsForAddress($args['input']['address'], $cart); diff --git a/vendor/magento/module-quote-graph-ql/Model/Resolver/EstimateTotals.php b/vendor/magento/module-quote-graph-ql/Model/Resolver/EstimateTotals.php index af155594207fb..1402319331b0a 100644 --- a/vendor/magento/module-quote-graph-ql/Model/Resolver/EstimateTotals.php +++ b/vendor/magento/module-quote-graph-ql/Model/Resolver/EstimateTotals.php @@ -1,6 +1,6 @@ $args['input']['cart_id'] ] - ) + ), + $exception, + $this->errorMapper->getErrorMessageId('Could not find a cart with ID') ); } diff --git a/vendor/magento/module-quote-graph-ql/Model/Resolver/PlaceOrder.php b/vendor/magento/module-quote-graph-ql/Model/Resolver/PlaceOrder.php index 5a4d66d306234..39bc4881b81a5 100644 --- a/vendor/magento/module-quote-graph-ql/Model/Resolver/PlaceOrder.php +++ b/vendor/magento/module-quote-graph-ql/Model/Resolver/PlaceOrder.php @@ -1,7 +1,7 @@ self::ERROR_CART_NOT_FOUND, - 'The cart isn\'t active' => self::ERROR_CART_NOT_ACTIVE, - 'Guest email for cart is missing' => self::ERROR_GUEST_EMAIL_MISSING, - 'A server error stopped your order from being placed. Please try to place your order again' => - self::ERROR_UNABLE_TO_PLACE_ORDER, - 'Some addresses can\'t be used due to the configurations for specific countries' => - self::ERROR_UNABLE_TO_PLACE_ORDER, - 'The shipping method is missing. Select the shipping method and try again' => - self::ERROR_UNABLE_TO_PLACE_ORDER, - 'Please check the billing address information' => self::ERROR_UNABLE_TO_PLACE_ORDER, - 'Enter a valid payment method and try again' => self::ERROR_UNABLE_TO_PLACE_ORDER, - 'Some of the products are out of stock' => self::ERROR_UNABLE_TO_PLACE_ORDER, - ]; - - /** - * @var \string[] - */ - private $errors = []; - /** * @param GetCartForCheckout $getCartForCheckout * @param PlaceOrderModel $placeOrder * @param OrderRepositoryInterface $orderRepository * @param OrderFormatter $orderFormatter + * @param OrderErrorProcessor $orderErrorProcessor */ public function __construct( private readonly GetCartForCheckout $getCartForCheckout, private readonly PlaceOrderModel $placeOrder, private readonly OrderRepositoryInterface $orderRepository, - private readonly OrderFormatter $orderFormatter + private readonly OrderFormatter $orderFormatter, + private readonly OrderErrorProcessor $orderErrorProcessor ) { } @@ -78,8 +46,6 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, ?array $value = null, ?array $args = null) { - $this->errors = []; - $order = null; if (empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } @@ -87,83 +53,24 @@ public function resolve(Field $field, $context, ResolveInfo $info, ?array $value $maskedCartId = $args['input']['cart_id']; $userId = (int)$context->getUserId(); $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); - + $order = null; try { $cart = $this->getCartForCheckout->execute($maskedCartId, $userId, $storeId); $orderId = $this->placeOrder->execute($cart, $maskedCartId, $userId); $order = $this->orderRepository->get($orderId); - } catch (NoSuchEntityException $exception) { - $this->addError($exception->getMessage()); - } catch (GraphQlInputException $exception) { - $this->addError($exception->getMessage()); } catch (AuthorizationException $exception) { - throw new GraphQlAuthorizationException( - __($exception->getMessage()) - ); - } catch (LocalizedException $e) { - $this->addError($e->getMessage()); - } - if ($this->errors) { - return [ - 'errors' => - $this->errors - ]; + throw new GraphQlAuthorizationException(__($exception->getMessage())); + } catch (LocalizedException $exception) { + $this->orderErrorProcessor->execute($exception, $field, $context, $info); } + return [ 'order' => [ - 'order_number' => $order->getIncrementId(), + 'order_number' => $order?->getIncrementId(), // @deprecated The order_id field is deprecated, use order_number instead - 'order_id' => $order->getIncrementId(), + 'order_id' => $order?->getIncrementId(), ], - 'orderV2' => $this->orderFormatter->format($order), - 'errors' => [] + 'orderV2' => $order ? $this->orderFormatter->format($order) : null ]; } - - /** - * Add order line item error - * - * @param string $message - * @return void - */ - private function addError(string $message): void - { - $this->errors[] = [ - 'message' => $message, - 'code' => $this->getErrorCode($message) - ]; - } - - /** - * Get message error code. Ad-hoc solution based on message parsing. - * - * @param string $message - * @return string - */ - private function getErrorCode(string $message): string - { - $code = self::ERROR_UNDEFINED; - - $matchedCodes = array_filter( - self::MESSAGE_CODES, - function ($key) use ($message) { - return false !== strpos($message, $key); - }, - ARRAY_FILTER_USE_KEY - ); - - if (!empty($matchedCodes)) { - $code = current($matchedCodes); - } - - return $code; - } - - /** - * @inheritDoc - */ - public function _resetState(): void - { - $this->errors = []; - } } diff --git a/vendor/magento/module-quote-graph-ql/Model/Resolver/RemoveItemFromCart.php b/vendor/magento/module-quote-graph-ql/Model/Resolver/RemoveItemFromCart.php index 783c4bd196122..f66b9df31e6c8 100644 --- a/vendor/magento/module-quote-graph-ql/Model/Resolver/RemoveItemFromCart.php +++ b/vendor/magento/module-quote-graph-ql/Model/Resolver/RemoveItemFromCart.php @@ -1,7 +1,7 @@ getCartForUser = $getCartForUser; $this->cartItemRepository = $cartItemRepository; $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; $this->argsSelection = $argsSelection; + $this->errorMapper = $errorMapper; } /** @@ -76,7 +85,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, ?array $value $cartId = $this->maskedQuoteIdToQuoteId->execute($maskedCartId); } catch (NoSuchEntityException $exception) { throw new GraphQlNoSuchEntityException( - __('Could not find a cart with ID "%masked_cart_id"', ['masked_cart_id' => $maskedCartId]) + __('Could not find a cart with ID "%masked_cart_id"', ['masked_cart_id' => $maskedCartId]), + $exception, + $this->errorMapper->getErrorMessageId('Could not find a cart with ID') ); } diff --git a/vendor/magento/module-quote-graph-ql/etc/graphql/di.xml b/vendor/magento/module-quote-graph-ql/etc/graphql/di.xml index f997e7be65685..69419dbbf3c05 100644 --- a/vendor/magento/module-quote-graph-ql/etc/graphql/di.xml +++ b/vendor/magento/module-quote-graph-ql/etc/graphql/di.xml @@ -44,7 +44,7 @@ - + Magento\QuoteGraphQl\Helper\Error\PlaceOrderMessageFormatter @@ -99,4 +99,11 @@ + + + + Magento\QuoteGraphQl\Model\OrderResponseFormatter + + + diff --git a/vendor/magento/module-quote-graph-ql/etc/schema.graphqls b/vendor/magento/module-quote-graph-ql/etc/schema.graphqls index d1cec83915378..afc23857e7066 100644 --- a/vendor/magento/module-quote-graph-ql/etc/schema.graphqls +++ b/vendor/magento/module-quote-graph-ql/etc/schema.graphqls @@ -270,6 +270,14 @@ type PlaceOrderError @doc(description:"An error encountered while placing an ord code: PlaceOrderErrorCodes! @doc(description: "An error code that is specific to place order.") } +enum PlaceOrderErrorCodes { + CART_NOT_FOUND + CART_NOT_ACTIVE + GUEST_EMAIL_MISSING + UNABLE_TO_PLACE_ORDER + UNDEFINED +} + type Cart @doc(description: "Contains the contents and other details about a guest or customer cart.") { id: ID! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\MaskedCartId") @doc(description: "The unique ID for a `Cart` object.") items: [CartItemInterface] @deprecated(reason: "Use `itemsV2` instead.") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItems") @doc(description: "An array of products that have been added to the cart.") @@ -517,13 +525,6 @@ enum CartUserInputErrorType { INVALID_PARAMETER_VALUE UNDEFINED } -enum PlaceOrderErrorCodes { - CART_NOT_FOUND - CART_NOT_ACTIVE - GUEST_EMAIL_MISSING - UNABLE_TO_PLACE_ORDER - UNDEFINED -} type StoreConfig { is_guest_checkout_enabled: Boolean @doc(description: "checkout/options/guest_checkout: whether the guest checkout is enabled or not.") diff --git a/vendor/magento/framework/GraphQl/Query/QueryDataFormatter.php b/vendor/magento/framework/GraphQl/Query/QueryDataFormatter.php new file mode 100644 index 0000000000000..eb566c71b568f --- /dev/null +++ b/vendor/magento/framework/GraphQl/Query/QueryDataFormatter.php @@ -0,0 +1,45 @@ +formatterPool[] = $formatter; + } else { + throw new \InvalidArgumentException( + sprintf('Formatter must implement %s', QueryResponseFormatterInterface::class) + ); + } + } + } + /** + * Format the response using registered formatters + * + * @param array $executionResult + * @return array + */ + public function formatResponse(array $executionResult): array + { + foreach ($this->formatterPool as $formatter) { + $executionResult = $formatter->formatResponse($executionResult); + } + return $executionResult; + } +} diff --git a/vendor/magento/framework/GraphQl/Query/QueryProcessor.php b/vendor/magento/framework/GraphQl/Query/QueryProcessor.php index f264d31095919..e013df03306d9 100644 --- a/vendor/magento/framework/GraphQl/Query/QueryProcessor.php +++ b/vendor/magento/framework/GraphQl/Query/QueryProcessor.php @@ -37,6 +37,11 @@ class QueryProcessor */ private $errorHandler; + /** + * @var QueryDataFormatter + */ + private QueryDataFormatter $formatter; + /** * @var QueryParser */ @@ -46,6 +51,7 @@ class QueryProcessor * @param ExceptionFormatter $exceptionFormatter * @param QueryComplexityLimiter $queryComplexityLimiter * @param ErrorHandlerInterface $errorHandler + * @param QueryDataFormatter $formatter * @param QueryParser|null $queryParser * @SuppressWarnings(PHPMD.LongVariable) */ @@ -53,11 +59,13 @@ public function __construct( ExceptionFormatter $exceptionFormatter, QueryComplexityLimiter $queryComplexityLimiter, ErrorHandlerInterface $errorHandler, + QueryDataFormatter $formatter, ?QueryParser $queryParser = null ) { $this->exceptionFormatter = $exceptionFormatter; $this->queryComplexityLimiter = $queryComplexityLimiter; $this->errorHandler = $errorHandler; + $this->formatter = $formatter; $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); } @@ -89,7 +97,7 @@ public function process( } $rootValue = null; - return GraphQL::executeQuery( + $executionResult = GraphQL::executeQuery( $schema, $source, $rootValue, @@ -101,5 +109,7 @@ public function process( )->toArray( (int) ($this->exceptionFormatter->shouldShowDetail() ? DebugFlag::INCLUDE_DEBUG_MESSAGE : false) ); + + return $this->formatter->formatResponse($executionResult); } } diff --git a/vendor/magento/framework/GraphQl/Query/QueryResponseFormatterInterface.php b/vendor/magento/framework/GraphQl/Query/QueryResponseFormatterInterface.php new file mode 100644 index 0000000000000..a275037ede555 --- /dev/null +++ b/vendor/magento/framework/GraphQl/Query/QueryResponseFormatterInterface.php @@ -0,0 +1,19 @@ +