diff --git a/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php b/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php new file mode 100644 index 000000000000..8733b9a6b1d0 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php @@ -0,0 +1,59 @@ +_dateExists() || $this->_timeExists()) { + return parent::validateUserValue($this->formatValues($values)); + } + + return $this; + } + + /** + * @param array $values + * @return array mixed + */ + protected function formatValues($values) + { + if (isset($values[$this->getOption()->getId()])) { + $value = $values[$this->getOption()->getId()]; + $dateTime = \DateTime::createFromFormat(DateTime::DATETIME_PHP_FORMAT, $value); + $values[$this->getOption()->getId()] = [ + 'date' => $value, + 'year' => $dateTime->format('Y'), + 'month' => $dateTime->format('m'), + 'day' => $dateTime->format('d'), + 'hour' => $dateTime->format('H'), + 'minute' => $dateTime->format('i'), + 'day_part' => $dateTime->format('a'), + ]; + } + + return $values; + } + + /** + * @return bool + */ + public function useCalendar() + { + return false; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Hydrator/CartHydrator.php b/app/code/Magento/QuoteGraphQl/Model/Hydrator/CartHydrator.php new file mode 100644 index 000000000000..5f386134bcc9 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Hydrator/CartHydrator.php @@ -0,0 +1,47 @@ +getAllItems() as $cartItem) { + $productData = $cartItem->getProduct()->getData(); + $productData['model'] = $cartItem->getProduct(); + + $items[] = [ + 'id' => $cartItem->getItemId(), + 'qty' => $cartItem->getQty(), + 'product' => $productData, + 'model' => $cartItem, + ]; + } + + return [ + 'items' => $items, + ]; + } +} \ No newline at end of file diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart/AddSimpleProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart/AddSimpleProductsToCart.php new file mode 100644 index 000000000000..49bbcb1e4372 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart/AddSimpleProductsToCart.php @@ -0,0 +1,226 @@ +valueFactory = $valueFactory; + $this->userContext = $userContext; + $this->arrayManager = $arrayManager; + $this->productRepository = $productRepository; + $this->cartHydrator = $cartHydrator; + $this->guestCartRepository = $guestCartRepository; + $this->dataObjectFactory = $dataObjectFactory; + $this->cartRepository = $cartRepository; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + } + + /** + * {@inheritDoc} + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + { + $cartHash = $this->arrayManager->get('input/cart_id', $args); + $cartItems = $this->arrayManager->get('input/cartItems', $args); + + if (!isset($cartHash)) { + throw new GraphQlInputException( + __('Missing key %1 in cart data', ['cart_id']) + ); + } + + if (!isset($cartItems)) { + throw new GraphQlInputException( + __('Missing key %1 in cart data', ['cartItems']) + ); + } + + $cart = $this->getCart((string) $cartHash); + + foreach ($cartItems as $cartItem) { + $sku = $this->arrayManager->get('details/sku', $cartItem); + $product = $this->productRepository->get($sku); + + $message = $cart->addProduct($product, $this->getBuyRequest($cartItem)); + + if (is_string($message)) { + throw new GraphQlInputException( + __('%1: %2', $sku, $message) + ); + } + + if ($cart->getData('has_error')) { + throw new GraphQlInputException( + __('%1: %2', $sku, $this->getCartErrors($cart)) + ); + } + } + + $this->cartRepository->save($cart); + + $result = function () use ($cart) { + return [ + 'cart' => $this->cartHydrator->hydrate($cart) + ]; + }; + + return $this->valueFactory->create($result); + } + + /** + * Format GraphQl input data to a shape that buy request has + * + * @param array $cartItem + * @return DataObject + */ + private function getBuyRequest($cartItem): DataObject + { + $customOptions = []; + $qty = $this->arrayManager->get('details/qty', $cartItem); + $customizableOptions = $this->arrayManager->get('customizable_options', $cartItem, []); + + foreach ($customizableOptions as $customizableOption) { + $customOptions[$customizableOption['id']] = $customizableOption['value']; + } + + return $this->dataObjectFactory->create([ + 'data' => [ + 'qty' => $qty, + 'options' => $customOptions + ] + ]); + } + + /** + * Collecting cart errors + * + * @param CartInterface|Quote $cart + * @return string + */ + private function getCartErrors($cart): string + { + $errorMessages = []; + + /** @var AbstractMessage $error */ + foreach ($cart->getErrors() as $error) { + $errorMessages[] = $error->getText(); + } + + return implode(PHP_EOL, $errorMessages); + } + + /** + * Retrieving quote mode based on customer authorization + * + * @param string $cartHash + * @return CartInterface|Quote + * @throws NoSuchEntityException + */ + private function getCart(string $cartHash): CartInterface + { + $customerId = $this->userContext->getUserId(); + + if (!$customerId) { + return $this->guestCartRepository->get($cartHash); + } + + $cartId = $this->maskedQuoteIdToQuoteId->execute((string) $cartHash); + return $this->cartRepository->get($cartId); + } +} \ No newline at end of file diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItem/CustomizableOptions.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItem/CustomizableOptions.php new file mode 100644 index 000000000000..871d152faa86 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItem/CustomizableOptions.php @@ -0,0 +1,235 @@ +valueFactory = $valueFactory; + $this->userContext = $userContext; + $this->storeManager = $storeManager; + } + + /** + * {@inheritDoc} + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + { + if (!isset($value['model'])) { + return $this->valueFactory->create(function () { + return []; + }); + } + + /** @var QuoteItem $cartItem */ + $cartItem = $value['model']; + $optionIds = $cartItem->getOptionByCode('option_ids'); + + if (!$optionIds) { + return $this->valueFactory->create(function () { + return []; + }); + } + + $customOptions = []; + $customOptionIds = explode(',', $optionIds->getValue()); + + foreach ($customOptionIds as $optionId) { + $customOptionData = $this->getOptionData($cartItem, (int) $optionId); + + if (0 === count($customOptionData)) { + continue; + } + + $customOptions[] = $customOptionData; + } + + $result = function () use ($customOptions) { + return $customOptions; + }; + + return $this->valueFactory->create($result); + } + + /** + * @param QuoteItem $cartItem + * @param int $optionId + * @return array + * @throws NoSuchEntityException + * @throws LocalizedException + */ + private function getOptionData($cartItem, int $optionId): array + { + $product = $cartItem->getProduct(); + $option = $product->getOptionById($optionId); + + if (!$option) { + return []; + } + + $itemOption = $cartItem->getOptionByCode('option_' . $option->getId()); + + /** @var SelectOptionType|TextOptionType|DefaultOptionType $optionTypeGroup */ + $optionTypeGroup = $option->groupFactory($option->getType()) + ->setOption($option) + ->setConfigurationItem($cartItem) + ->setConfigurationItemOption($itemOption); + + if (ProductCustomOptionInterface::OPTION_GROUP_FILE == $option->getType()) { + $downloadParams = $cartItem->getFileDownloadParams(); + + if ($downloadParams) { + $url = $downloadParams->getUrl(); + if ($url) { + $optionTypeGroup->setCustomOptionDownloadUrl($url); + } + $urlParams = $downloadParams->getUrlParams(); + if ($urlParams) { + $optionTypeGroup->setCustomOptionUrlParams($urlParams); + } + } + } + + $selectedOptionValueData = [ + 'id' => $itemOption->getId(), + 'label' => $optionTypeGroup->getFormattedOptionValue($itemOption->getValue()), + ]; + + if (ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN == $option->getType() + || ProductCustomOptionInterface::OPTION_TYPE_RADIO == $option->getType() + || ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX == $option->getType() + ) { + $optionValue = $option->getValueById($itemOption->getValue()); + $priceValueUnits = $this->getPriceValueUnits($optionValue->getPriceType()); + + $selectedOptionValueData['price'] = [ + 'type' => strtoupper($optionValue->getPriceType()), + 'units' => $priceValueUnits, + 'value' => $optionValue->getPrice(), + ]; + + $selectedOptionValueData = [$selectedOptionValueData]; + } + + if (ProductCustomOptionInterface::OPTION_TYPE_FIELD == $option->getType() + || ProductCustomOptionInterface::OPTION_TYPE_AREA == $option->getType() + || ProductCustomOptionInterface::OPTION_GROUP_DATE == $option->getType() + || ProductCustomOptionInterface::OPTION_TYPE_TIME == $option->getType() + ) { + $priceValueUnits = $this->getPriceValueUnits($option->getPriceType()); + + $selectedOptionValueData['price'] = [ + 'type' => strtoupper($option->getPriceType()), + 'units' => $priceValueUnits, + 'value' => $option->getPrice(), + ]; + + $selectedOptionValueData = [$selectedOptionValueData]; + } + + if (ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE == $option->getType()) { + $selectedOptionValueData = []; + $optionIds = explode(',', $itemOption->getValue()); + + foreach ($optionIds as $optionId) { + $optionValue = $option->getValueById($optionId); + $priceValueUnits = $this->getPriceValueUnits($optionValue->getPriceType()); + + $selectedOptionValueData[] = [ + 'id' => $itemOption->getId(), + 'label' => $optionValue->getTitle(), + 'price' => [ + 'type' => strtoupper($optionValue->getPriceType()), + 'units' => $priceValueUnits, + 'value' => $optionValue->getPrice(), + ], + ]; + } + } + + return [ + 'id' => $option->getId(), + 'label' => $option->getTitle(), + 'type' => $option->getType(), + 'values' => $selectedOptionValueData, + 'sort_order' => $option->getSortOrder(), + ]; + } + + /** + * @param string $priceType + * @return string + * @throws NoSuchEntityException + */ + private function getPriceValueUnits(string $priceType): string + { + if (ProductPriceOptionsInterface::VALUE_PERCENT == $priceType) { + return '%'; + } + + return $this->getCurrencySymbol(); + } + + /** + * Get currency symbol + * @return string + * @throws NoSuchEntityException + */ + private function getCurrencySymbol(): string + { + /** @var Store|StoreInterface $store */ + $store = $this->storeManager->getStore(); + + return $store->getBaseCurrency()->getCurrencySymbol(); + } +} \ No newline at end of file diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemTypeResolver.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemTypeResolver.php new file mode 100644 index 000000000000..75232190b736 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemTypeResolver.php @@ -0,0 +1,55 @@ +cartItemTypes = $cartItemTypes; + } + + /** + * {@inheritdoc} + * @throws GraphQlInputException + */ + public function resolveType(array $data) : string + { + if (!isset($data['product'])) { + return ''; + } + + $productData = $data['product']; + + if (!isset($productData['type_id'])) { + return ''; + } + + $productTypeId = $productData['type_id']; + + if (!isset($this->cartItemTypes[$productTypeId])) { + return ''; + } + + return $this->cartItemTypes[$productTypeId]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemTypeResolverComposite.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemTypeResolverComposite.php new file mode 100644 index 000000000000..264887a66f62 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemTypeResolverComposite.php @@ -0,0 +1,55 @@ +cartItemTypeResolvers = $cartItemTypeResolvers; + } + + /** + * {@inheritdoc} + * @throws GraphQlInputException + */ + public function resolveType(array $data) : string + { + if (!isset($data['product'])) { + throw new GraphQlInputException( + __('Missing key %1 in cart data', ['product']) + ); + } + + foreach ($this->cartItemTypeResolvers as $cartItemTypeResolver) { + $resolvedType = $cartItemTypeResolver->resolveType($data); + + if ($resolvedType) { + return $resolvedType; + } + } + + throw new GraphQlInputException( + __('Concrete type for %1 not implemented', ['CartItemInterface']) + ); + } +} diff --git a/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml new file mode 100644 index 000000000000..0a13f1ba3455 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml @@ -0,0 +1,16 @@ + + + + + + + Magento\QuoteGraphQl\Model\Resolver\CartItemTypeResolver + + + + diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 46d1b97d0aea..8ee32e493c0d 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -2,5 +2,41 @@ # See COPYING.txt for license details. type Mutation { - createEmptyCart: String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Cart\\CreateEmptyCart") @doc(description:"Creates empty shopping cart for guest or logged in user") + createEmptyCart: String @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\Cart\\CreateEmptyCart") @doc(description:"Creates empty shopping cart for guest or logged in user") } + +type Cart { + items: [CartItemInterface] +} + +input CartItemDetailsInput { + sku: String! + qty: Float! +} + +interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolverComposite") { + id: String! + qty: Float! + product: ProductInterface! +} + +type SelectedCustomizableOption { + id: Int! + label: String! + type: String! + values: [SelectedCustomizableOptionValue!]! + sort_order: Int! +} + +type SelectedCustomizableOptionValue { + id: Int + label: String! + price: CartItemSelectedOptionValuePrice! + sort_order: Int! +} + +type CartItemSelectedOptionValuePrice { + value: Float! + units: String! + type: PriceTypeEnum! +} \ No newline at end of file