diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index b491b10730c10..3f6cc42614030 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -8,8 +8,6 @@ namespace Magento\QuoteGraphQl\Model\Cart; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\DataObject; -use Magento\Framework\DataObjectFactory; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; @@ -21,9 +19,9 @@ class AddSimpleProductToCart { /** - * @var DataObjectFactory + * @var CreateBuyRequest */ - private $dataObjectFactory; + private $createBuyRequest; /** * @var ProductRepositoryInterface @@ -31,15 +29,15 @@ class AddSimpleProductToCart private $productRepository; /** - * @param DataObjectFactory $dataObjectFactory * @param ProductRepositoryInterface $productRepository + * @param CreateBuyRequest $createBuyRequest */ public function __construct( - DataObjectFactory $dataObjectFactory, - ProductRepositoryInterface $productRepository + ProductRepositoryInterface $productRepository, + CreateBuyRequest $createBuyRequest ) { - $this->dataObjectFactory = $dataObjectFactory; $this->productRepository = $productRepository; + $this->createBuyRequest = $createBuyRequest; } /** @@ -56,7 +54,7 @@ public function execute(Quote $cart, array $cartItemData): void { $sku = $this->extractSku($cartItemData); $quantity = $this->extractQuantity($cartItemData); - $customizableOptions = $this->extractCustomizableOptions($cartItemData); + $customizableOptions = $cartItemData['customizable_options'] ?? []; try { $product = $this->productRepository->get($sku); @@ -65,7 +63,7 @@ public function execute(Quote $cart, array $cartItemData): void } try { - $result = $cart->addProduct($product, $this->createBuyRequest($quantity, $customizableOptions)); + $result = $cart->addProduct($product, $this->createBuyRequest->execute($quantity, $customizableOptions)); } catch (\Exception $e) { throw new GraphQlInputException( __( @@ -116,60 +114,4 @@ private function extractQuantity(array $cartItemData): float } return $quantity; } - - /** - * Extract Customizable Options from cart item data - * - * @param array $cartItemData - * @return array - */ - private function extractCustomizableOptions(array $cartItemData): array - { - if (!isset($cartItemData['customizable_options']) || empty($cartItemData['customizable_options'])) { - return []; - } - - $customizableOptionsData = []; - foreach ($cartItemData['customizable_options'] as $customizableOption) { - if (isset($customizableOption['value_string'])) { - $customizableOptionsData[$customizableOption['id']] = $this->convertCustomOptionValue( - $customizableOption['value_string'] - ); - } - } - return $customizableOptionsData; - } - - /** - * Format GraphQl input data to a shape that buy request has - * - * @param float $quantity - * @param array $customOptions - * @return DataObject - */ - private function createBuyRequest(float $quantity, array $customOptions): DataObject - { - return $this->dataObjectFactory->create([ - 'data' => [ - 'qty' => $quantity, - 'options' => $customOptions, - ], - ]); - } - - /** - * Convert custom options vakue - * - * @param string $value - * @return string|array - */ - private function convertCustomOptionValue(string $value) - { - $value = trim($value); - if (substr($value, 0, 1) === "[" && - substr($value, strlen($value) - 1, 1) === "]") { - return explode(',', substr($value, 1, -1)); - } - return $value; - } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php new file mode 100644 index 0000000000000..b95be8db2dccd --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php @@ -0,0 +1,75 @@ +dataObjectFactory = $dataObjectFactory; + } + + /** + * Returns buy request for working with cart items + * + * @param float $qty + * @param array $customizableOptionsData + * @return DataObject + */ + public function execute(float $qty, array $customizableOptionsData): DataObject + { + $customizableOptions = []; + foreach ($customizableOptionsData as $customizableOption) { + if (isset($customizableOption['value_string'])) { + $customizableOptions[$customizableOption['id']] = $this->convertCustomOptionValue( + $customizableOption['value_string'] + ); + } + } + + return $this->dataObjectFactory->create( + [ + 'data' => [ + 'qty' => $qty, + 'options' => $customizableOptions, + ], + ] + ); + } + + /** + * Convert custom options value + * + * @param string $value + * @return string|array + */ + private function convertCustomOptionValue(string $value) + { + $value = trim($value); + if (substr($value, 0, 1) === "[" && + substr($value, strlen($value) - 1, 1) === "]") { + return explode(',', substr($value, 1, -1)); + } + return $value; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php b/app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php new file mode 100644 index 0000000000000..b18c6ad662335 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php @@ -0,0 +1,147 @@ +cartItemRepository = $cartItemRepository; + $this->quoteRepository = $quoteRepository; + $this->createBuyRequest = $createBuyRequest; + } + + /** + * Update cart item + * + * @param Quote $cart + * @param int $cartItemId + * @param float $quantity + * @param array $customizableOptionsData + * @return void + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + * @throws NoSuchEntityException + */ + public function execute(Quote $cart, int $cartItemId, float $quantity, array $customizableOptionsData): void + { + if (count($customizableOptionsData) === 0) { // Update only item's qty + $this->updateItemQuantity($cartItemId, $cart, $quantity); + + return; + } + + try { + $result = $cart->updateItem( + $cartItemId, + $this->createBuyRequest->execute($quantity, $customizableOptionsData) + ); + } catch (LocalizedException $e) { + throw new GraphQlInputException( + __( + 'Could not update cart item: %message', + ['message' => $e->getMessage()] + ) + ); + } + + if ($result->getHasError()) { + throw new GraphQlInputException( + __( + 'Could not update cart item: %message', + ['message' => $result->getMessage()] + ) + ); + } + + $this->quoteRepository->save($cart); + } + + /** + * Updates item qty for the specified cart + * + * @param int $itemId + * @param Quote $cart + * @param float $quantity + * @throws GraphQlNoSuchEntityException + * @throws NoSuchEntityException + * @throws GraphQlNoSuchEntityException + */ + private function updateItemQuantity(int $itemId, Quote $cart, float $quantity) + { + $cartItem = $cart->getItemById($itemId); + if ($cartItem === false) { + throw new GraphQlNoSuchEntityException( + __('Could not find cart item with id: %1.', $itemId) + ); + } + $cartItem->setQty($quantity); + $this->validateCartItem($cartItem); + $this->cartItemRepository->save($cartItem); + } + + /** + * Validate cart item + * + * @param Item $cartItem + * @return void + * @throws GraphQlInputException + */ + private function validateCartItem(Item $cartItem): void + { + if ($cartItem->getHasError()) { + $errors = []; + foreach ($cartItem->getMessage(false) as $message) { + $errors[] = $message; + } + if (!empty($errors)) { + throw new GraphQlInputException( + __( + 'Could not update the product with SKU %sku: %message', + ['sku' => $cartItem->getSku(), 'message' => __(implode("\n", $errors))] + ) + ); + } + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php index fa115db144e32..db6a43513cc30 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php @@ -16,14 +16,19 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Api\CartItemRepositoryInterface; use Magento\Quote\Model\Quote; -use Magento\Quote\Model\Quote\Item; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem; /** * @inheritdoc */ class UpdateCartItems implements ResolverInterface { + /** + * @var UpdateCartItem + */ + private $updateCartItem; + /** * @var GetCartForUser */ @@ -37,13 +42,16 @@ class UpdateCartItems implements ResolverInterface /** * @param GetCartForUser $getCartForUser * @param CartItemRepositoryInterface $cartItemRepository + * @param UpdateCartItem $updateCartItem */ public function __construct( GetCartForUser $getCartForUser, - CartItemRepositoryInterface $cartItemRepository + CartItemRepositoryInterface $cartItemRepository, + UpdateCartItem $updateCartItem ) { $this->getCartForUser = $getCartForUser; $this->cartItemRepository = $cartItemRepository; + $this->updateCartItem = $updateCartItem; } /** @@ -94,52 +102,18 @@ private function processCartItems(Quote $cart, array $items): void if (empty($item['cart_item_id'])) { throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.')); } - $itemId = $item['cart_item_id']; + $itemId = (int)$item['cart_item_id']; + $customizableOptions = $item['customizable_options'] ?? []; - if (!isset($item['quantity'])) { + if (count($customizableOptions) === 0 && !isset($item['quantity'])) { throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.')); } $quantity = (float)$item['quantity']; - $cartItem = $cart->getItemById($itemId); - if ($cartItem === false) { - throw new GraphQlNoSuchEntityException( - __('Could not find cart item with id: %1.', $item['cart_item_id']) - ); - } - if ($quantity <= 0.0) { $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); } else { - $cartItem->setQty($quantity); - $this->validateCartItem($cartItem); - $this->cartItemRepository->save($cartItem); - } - } - } - - /** - * Validate cart item - * - * @param Item $cartItem - * @return void - * @throws GraphQlInputException - */ - private function validateCartItem(Item $cartItem): void - { - if ($cartItem->getHasError()) { - $errors = []; - foreach ($cartItem->getMessage(false) as $message) { - $errors[] = $message; - } - - if (!empty($errors)) { - throw new GraphQlInputException( - __( - 'Could not update the product with SKU %sku: %message', - ['sku' => $cartItem->getSku(), 'message' => __(implode("\n", $errors))] - ) - ); + $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions); } } } diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index ea5e34ff1e34a..893a0b9df458d 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -68,7 +68,8 @@ input UpdateCartItemsInput { input CartItemUpdateInput { cart_item_id: Int! - quantity: Float! + quantity: Float + customizable_options: [CustomizableOptionInput!] } input RemoveItemFromCartInput { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php index bc88bd1ddb438..48ea4ab7a15e3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php @@ -285,7 +285,7 @@ public function dataProviderUpdateWithMissedRequiredParameters(): array ], 'missed_cart_item_qty' => [ 'cart_items: [{ cart_item_id: 1 }]', - 'Field CartItemUpdateInput.quantity of required type Float! was not provided.' + 'Required parameter "quantity" for "cart_items" is missing.' ], ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php new file mode 100644 index 0000000000000..62c1ae0dab3c7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php @@ -0,0 +1,273 @@ +getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->productCustomOptionsRepository = $objectManager->get(ProductCustomOptionRepositoryInterface::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/set_custom_options_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product_with_options.php + */ + public function testChangeQuoteItemCustomOptions() + { + $sku = 'simple_product'; + $quoteItemId = $this->getQuoteItemIdBySku($sku); + $customOptionsValues = $this->getCustomOptionsValuesForQuery($sku); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $customizableOptionsQuery = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); + + $query = $this->getQuery($maskedQuoteId, $quoteItemId, $customizableOptionsQuery); + $response = $this->graphQlMutation($query); + $itemOptionsResponse = $response['updateCartItems']['cart']['items'][0]['customizable_options']; + self::assertCount(2, $itemOptionsResponse); + self::assertEquals('test', $itemOptionsResponse[0]['values'][0]['value']); + self::assertEquals('test', $itemOptionsResponse[1]['values'][0]['value']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/set_custom_options_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product_with_options.php + */ + public function testOptionsSetPersistsOnQtyChange() + { + $sku = 'simple_product'; + $newQuantity = 2; + $quoteItemId = $this->getQuoteItemIdBySku($sku); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<graphQlMutation($query); + $cartItemResponse = $response['updateCartItems']['cart']['items'][0]; + + self::assertEquals($newQuantity, $cartItemResponse['quantity']); + self::assertCount(2, $cartItemResponse['customizable_options']); + self::assertEquals('initial value', $cartItemResponse['customizable_options'][0]['values'][0]['value']); + self::assertEquals('initial value', $cartItemResponse['customizable_options'][1]['values'][0]['value']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/set_custom_options_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product_with_options.php + */ + public function testOptionsSetChangedOnChangeOneOption() + { + $sku = 'simple_product'; + $quoteItemId = $this->getQuoteItemIdBySku($sku); + + /* Get only the first option */ + $customOptionsValues = array_slice($this->getCustomOptionsValuesForQuery($sku), 0, 1); + + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $customizableOptionsQuery = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); + $query = $this->getQuery($maskedQuoteId, $quoteItemId, $customizableOptionsQuery); + + $response = $this->graphQlMutation($query); + $itemOptionsResponse = $response['updateCartItems']['cart']['items'][0]['customizable_options']; + self::assertCount(1, $itemOptionsResponse); + self::assertEquals('test', $itemOptionsResponse[0]['values'][0]['value']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/set_custom_options_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product_with_options.php + */ + public function testOptionSetPersistsOnExtraOptionWithIncorrectId() + { + $sku = 'simple_product'; + $quoteItemId = $this->getQuoteItemIdBySku($sku); + $customOptionsValues = $this->getCustomOptionsValuesForQuery($sku); + + /* Add nonexistent option to the query */ + $customOptionsValues[] = ['id' => -10, 'value_string' => 'value']; + + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $customizableOptionsQuery = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); + $query = $this->getQuery($maskedQuoteId, $quoteItemId, $customizableOptionsQuery); + + $response = $this->graphQlMutation($query); + $itemOptionsResponse = $response['updateCartItems']['cart']['items'][0]['customizable_options']; + self::assertCount(2, $itemOptionsResponse); + } + + /** + * Returns GraphQl query for updating items in shopping cart + * + * @param string $maskedQuoteId + * @param int $quoteItemId + * @param $customizableOptionsQuery + * @return string + */ + private function getQuery(string $maskedQuoteId, int $quoteItemId, $customizableOptionsQuery): string + { + return <<quoteFactory->create(); + $product = $this->productRepository->get($sku); + $this->quoteResource->load($quote, 'test_quote', 'reserved_order_id'); + /** @var Item $quoteItem */ + $quoteItem = $quote->getItemByProduct($product); + + return (int)$quoteItem->getId(); + } + + /** + * Generate an array with test values for customizable options + * based on the option type + * + * @param string $sku + * @return array + */ + private function getCustomOptionsValuesForQuery(string $sku): array + { + $customOptions = $this->productCustomOptionsRepository->getList($sku); + $customOptionsValues = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + if ($optionType == 'field' || $optionType == 'area') { + $customOptionsValues[] = [ + 'id' => (int) $customOption->getOptionId(), + 'value_string' => 'test' + ]; + } elseif ($optionType == 'drop_down') { + $optionSelectValues = $customOption->getValues(); + $customOptionsValues[] = [ + 'id' => (int) $customOption->getOptionId(), + 'value_string' => reset($optionSelectValues)->getOptionTypeId() + ]; + } + } + + return $customOptionsValues; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php index 1e1fb0a176992..988ead7d86df3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php @@ -236,7 +236,7 @@ public function dataProviderUpdateWithMissedRequiredParameters(): array ], 'missed_cart_item_qty' => [ 'cart_items: [{ cart_item_id: 1 }]', - 'Field CartItemUpdateInput.quantity of required type Float! was not provided.' + 'Required parameter "quantity" for "cart_items" is missing.' ], ]; } diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/set_custom_options_simple_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/set_custom_options_simple_product.php new file mode 100644 index 0000000000000..618ed0e25602f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/set_custom_options_simple_product.php @@ -0,0 +1,54 @@ + 'test_option_code_1', + 'type' => 'field', + 'is_require' => false, + 'sort_order' => 1, + 'price' => -10.0, + 'price_type' => 'fixed', + 'sku' => 'sku1', + 'max_characters' => 100, + ], + [ + 'title' => 'area option', + 'type' => 'area', + 'is_require' => false, + 'sort_order' => 2, + 'price' => 20.0, + 'price_type' => 'percent', + 'sku' => 'sku2', + 'max_characters' => 100 + ] +]; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->get('simple_product'); +/** @var ProductCustomOptionInterfaceFactory $customOptionFactory */ +$customOptionFactory = $objectManager->get(ProductCustomOptionInterfaceFactory::class); + +foreach ($optionsSet as $option) { + /** @var ProductCustomOptionInterface $customOption */ + $customOption = $customOptionFactory->create(['data' => $option]); + $customOption->setProductSku($product->getSku()); + + $productCustomOptions[] = $customOption; +} + +$product->setOptions($productCustomOptions); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product_with_options.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product_with_options.php new file mode 100644 index 0000000000000..ecd1428a5d617 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product_with_options.php @@ -0,0 +1,53 @@ +get(ProductRepositoryInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); +/** @var OptionFactory $productOptionFactory */ +$productOptionFactory = Bootstrap::getObjectManager()->get(OptionFactory::class); +/** @var DataObjectFactory $dataObjectFactory */ +$dataObjectFactory = Bootstrap::getObjectManager()->get(DataObjectFactory::class); + +/** @var ProductOption $productOption */ +$productOption = $productOptionFactory->create(); +$product = $productRepository->get('simple_product'); +$productOptions = $productOption->getProductOptionCollection($product); +$cartItemCustomOptions = []; + +/** @var ProductOption $productOption */ +foreach ($productOptions as $productOption) { + $cartItemCustomOptions[$productOption->getId()] = 'initial value'; +} + +$request = $dataObjectFactory->create( + [ + 'data' => [ + 'qty' => 1.0, + 'options' => $cartItemCustomOptions, + ], + ] +); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->addProduct($product, $request); +$cartRepository->save($quote);