<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\Quote\Model\Cart;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder;
use Magento\Quote\Model\Cart\Data\AddProductsToCartOutput;
use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface;
use Magento\Quote\Model\Quote;
use Magento\Framework\Message\MessageInterface;
/**
* Unified approach to add products to the Shopping Cart.
* Client code must validate, that customer is eligible to call service with provided {cartId} and {cartItems}
*/
class AddProductsToCart
{
/**#@+
* Error message codes
*/
private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND';
private const ERROR_INSUFFICIENT_STOCK = 'INSUFFICIENT_STOCK';
private const ERROR_NOT_SALABLE = 'NOT_SALABLE';
private const ERROR_UNDEFINED = 'UNDEFINED';
/**#@-*/
/**
* List of error messages and codes.
*/
private const MESSAGE_CODES = [
'Could not find a product with SKU' => 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,
];
/**
* @var ProductRepositoryInterface
*/
private $productRepository;
/**
* @var CartRepositoryInterface
*/
private $cartRepository;
/**
* @var MaskedQuoteIdToQuoteIdInterface
*/
private $maskedQuoteIdToQuoteId;
/**
* @var BuyRequestBuilder
*/
private $requestBuilder;
/**
* @param ProductRepositoryInterface $productRepository
* @param CartRepositoryInterface $cartRepository
* @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId
* @param BuyRequestBuilder $requestBuilder
*/
public function __construct(
ProductRepositoryInterface $productRepository,
CartRepositoryInterface $cartRepository,
MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId,
BuyRequestBuilder $requestBuilder
) {
$this->productRepository = $productRepository;
$this->cartRepository = $cartRepository;
$this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId;
$this->requestBuilder = $requestBuilder;
}
/**
* Add cart items to the cart
*
* @param string $maskedCartId
* @param Data\CartItem[] $cartItems
* @return AddProductsToCartOutput
* @throws NoSuchEntityException Could not find a Cart with provided $maskedCartId
*/
public function execute(string $maskedCartId, array $cartItems): AddProductsToCartOutput
{
$cartId = $this->maskedQuoteIdToQuoteId->execute($maskedCartId);
$cart = $this->cartRepository->get($cartId);
$allErrors = [];
if ($cart->getData('has_error')) {
$errors = $cart->getErrors();
/** @var MessageInterface $error */
foreach ($errors as $error) {
$allErrors[] = $this->createError($error->getText());
}
}
$failedCartItems = $this->addItemsToCart($cart, $cartItems);
$saveCart = empty($failedCartItems);
if (!empty($failedCartItems)) {
/* Check if some cart items were successfully added to the cart */
if (count($failedCartItems) < count($cartItems)) {
/* Revert changes introduced by add to cart processes in case of an error */
$cart->getItemsCollection()->clear();
$newFailedCartItems = $this->addItemsToCart($cart, array_diff_key($cartItems, $failedCartItems));
$failedCartItems += $newFailedCartItems;
$saveCart = empty($newFailedCartItems);
}
foreach (array_keys($cartItems) as $cartItemPosition) {
if (isset($failedCartItems[$cartItemPosition])) {
array_push($allErrors, ...$failedCartItems[$cartItemPosition]);
}
}
}
if ($saveCart) {
$this->cartRepository->save($cart);
}
if (count($allErrors) !== 0) {
/* Revert changes introduced by add to cart processes in case of an error */
$cart->getItemsCollection()->clear();
}
return $this->prepareErrorOutput($cart, $allErrors);
}
/**
* Add cart items to cart
*
* @param Quote $cart
* @param array $cartItems
* @return array
*/
public function addItemsToCart(Quote $cart, array $cartItems): array
{
$failedCartItems = [];
foreach ($cartItems as $cartItemPosition => $cartItem) {
$errors = $this->addItemToCart($cart, $cartItem, $cartItemPosition);
if ($errors) {
$failedCartItems[$cartItemPosition] = $errors;
}
}
return $failedCartItems;
}
/**
* Adds a particular item to the shopping cart
*
* @param Quote $cart
* @param Data\CartItem $cartItem
* @param int $cartItemPosition
* @return array
*/
private function addItemToCart(Quote $cart, Data\CartItem $cartItem, int $cartItemPosition): array
{
$sku = $cartItem->getSku();
$errors = [];
$result = null;
$product = null;
if ($cartItem->getQuantity() <= 0) {
$errors[] = $this->createError(
__('The product quantity should be greater than 0')->render(),
$cartItemPosition
);
} else {
try {
$product = $this->productRepository->get($sku, false, null, true);
} catch (NoSuchEntityException $e) {
$errors[] = $this->createError(
__('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(),
$cartItemPosition
);
}
if ($product !== null) {
try {
$result = $cart->addProduct($product, $this->requestBuilder->build($cartItem));
} catch (\Throwable $e) {
$errors[] = $this->createError(
__($e->getMessage())->render(),
$cartItemPosition
);
}
if (is_string($result)) {
foreach (array_unique(explode("\n", $result)) as $error) {
$errors[] = $this->createError(__($error)->render(), $cartItemPosition);
}
}
}
}
return $errors;
}
/**
* Returns an error object
*
* @param string $message
* @param int $cartItemPosition
* @return Data\Error
*/
private function createError(string $message, int $cartItemPosition = 0): Data\Error
{
return new Data\Error(
$message,
$this->getErrorCode($message),
$cartItemPosition
);
}
/**
* Get message error code.
*
* TODO: introduce a separate class for getting error code from a message
*
* @param string $message
* @return string
*/
private function getErrorCode(string $message): string
{
foreach (self::MESSAGE_CODES as $codeMessage => $code) {
if (false !== stripos($message, $codeMessage)) {
return $code;
}
}
/* If no code was matched, return the default one */
return self::ERROR_UNDEFINED;
}
/**
* Creates a new output from existing errors
*
* @param Quote $cart
* @param array $errors
* @return AddProductsToCartOutput
*/
private function prepareErrorOutput(Quote $cart, array $errors = []): AddProductsToCartOutput
{
$output = new AddProductsToCartOutput($cart, $errors);
$cart->setHasError(false);
return $output;
}
}
|