diff --git a/core/components/minishop3/lexicon/en/cart.inc.php b/core/components/minishop3/lexicon/en/cart.inc.php index 8f632936d..7fbcdc67d 100644 --- a/core/components/minishop3/lexicon/en/cart.inc.php +++ b/core/components/minishop3/lexicon/en/cart.inc.php @@ -29,6 +29,7 @@ $_lang['ms3_cart_change_error'] = 'Error changing product count in cart'; $_lang['ms3_cart_change_options_success'] = 'Product options in cart successfully changed'; $_lang['ms3_cart_change_options_error'] = 'Error changing product options in cart'; +$_lang['ms3_err_cart_options'] = 'options must be an object or a JSON string'; $_lang['ms3_cart_clean_success'] = 'Cart successfully cleared'; $_lang['ms3_cart_is_empty'] = 'Your cart is empty'; diff --git a/core/components/minishop3/lexicon/en/default.inc.php b/core/components/minishop3/lexicon/en/default.inc.php index 163378ff0..12be2e6e0 100644 --- a/core/components/minishop3/lexicon/en/default.inc.php +++ b/core/components/minishop3/lexicon/en/default.inc.php @@ -168,6 +168,7 @@ $_lang['ms3_err_unknown'] = 'Unknown error'; $_lang['ms3_err_ns'] = 'This field is required'; $_lang['ms3_err_product_key_required'] = 'Product key is required'; +$_lang['ms3_err_cart_options'] = 'options must be an object or a JSON string'; $_lang['ms3_err_field_key_required'] = 'Field key is required'; $_lang['ms3_err_fields_required'] = 'Fields array is required'; $_lang['ms3_err_field_nf'] = 'Field not found'; diff --git a/core/components/minishop3/lexicon/ru/cart.inc.php b/core/components/minishop3/lexicon/ru/cart.inc.php index eb251fe9c..1bf9ec336 100644 --- a/core/components/minishop3/lexicon/ru/cart.inc.php +++ b/core/components/minishop3/lexicon/ru/cart.inc.php @@ -29,6 +29,7 @@ $_lang['ms3_cart_change_error'] = 'Ошибка при изменении количества товара в корзине'; $_lang['ms3_cart_change_options_success'] = 'Опции товара в корзине успешно изменены'; $_lang['ms3_cart_change_options_error'] = 'Ошибка при изменении опций товара в корзине'; +$_lang['ms3_err_cart_options'] = 'options должен быть объектом или JSON-строкой'; $_lang['ms3_cart_clean_success'] = 'Корзина успешно очищена'; $_lang['ms3_cart_is_empty'] = 'Ваша корзина пуста'; diff --git a/core/components/minishop3/lexicon/ru/default.inc.php b/core/components/minishop3/lexicon/ru/default.inc.php index 266ae656c..05e97d061 100644 --- a/core/components/minishop3/lexicon/ru/default.inc.php +++ b/core/components/minishop3/lexicon/ru/default.inc.php @@ -168,6 +168,7 @@ $_lang['ms3_err_unknown'] = 'Неизвестная ошибка'; $_lang['ms3_err_ns'] = 'Это поле обязательно'; $_lang['ms3_err_product_key_required'] = 'Не указан ключ товара'; +$_lang['ms3_err_cart_options'] = 'options должен быть объектом или JSON-строкой'; $_lang['ms3_err_field_key_required'] = 'Не указан ключ поля'; $_lang['ms3_err_fields_required'] = 'Требуется массив полей'; $_lang['ms3_err_field_nf'] = 'Поле не найдено'; diff --git a/core/components/minishop3/src/Controllers/Api/Web/CartController.php b/core/components/minishop3/src/Controllers/Api/Web/CartController.php index ebffe0f41..0c150012f 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CartController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CartController.php @@ -7,6 +7,7 @@ use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; use MiniShop3\Services\Api\WebApiContextResolver; +use MiniShop3\Services\Cart\CartItemManager; use MODX\Revolution\modX; /** @@ -24,7 +25,7 @@ class CartController public function __construct(modX $modx) { $this->modx = $modx; - $this->modx->lexicon->load('minishop3:customer', 'minishop3:default'); + $this->modx->lexicon->load('minishop3:customer', 'minishop3:default', 'minishop3:cart'); } /** @@ -117,14 +118,23 @@ public function changeOption(array $params = []): Response ); } - if (!is_array($options) || $options === []) { + if (!is_array($options) && !is_string($options)) { return Response::error( - $this->modx->lexicon('ms3_cart_change_options_error'), + $this->modx->lexicon('ms3_err_cart_options'), HttpStatus::BAD_REQUEST ); } $ms3 = $this->modx->services->get('ms3'); + $options = CartItemManager::normalizeOptions($options); + + if ($options === []) { + return Response::error( + $this->modx->lexicon('ms3_cart_change_options_error'), + HttpStatus::BAD_REQUEST + ); + } + $cart = $ms3->cart; $cart->initialize($this->pageContextKey(), $token); diff --git a/core/components/minishop3/src/Services/Cart/CartItemManager.php b/core/components/minishop3/src/Services/Cart/CartItemManager.php index 64db2bfd5..e94ff0b01 100644 --- a/core/components/minishop3/src/Services/Cart/CartItemManager.php +++ b/core/components/minishop3/src/Services/Cart/CartItemManager.php @@ -328,14 +328,15 @@ public function generateProductKey(array $product, array $options = []): string * @param mixed $options Options (array or JSON string) * @return array Normalized options */ - public function normalizeOptions(mixed $options): array + public static function normalizeOptions(mixed $options): array { - if (is_string($options)) { - $decoded = json_decode($options, true); - return is_array($decoded) ? $decoded : []; + if (!is_string($options)) { + return is_array($options) ? $options : []; } - return is_array($options) ? $options : []; + $decoded = json_decode($options, true); + + return is_array($decoded) ? $decoded : []; } /** diff --git a/core/components/minishop3/tests/Integration/WebApi/HeadlessStorefrontErrorsTest.php b/core/components/minishop3/tests/Integration/WebApi/HeadlessStorefrontErrorsTest.php index 546a3410f..763de2ecc 100644 --- a/core/components/minishop3/tests/Integration/WebApi/HeadlessStorefrontErrorsTest.php +++ b/core/components/minishop3/tests/Integration/WebApi/HeadlessStorefrontErrorsTest.php @@ -66,6 +66,52 @@ public function testChangeOptionEmptyOptionsReturns400(): void $this->assertApiError($res, HttpStatus::BAD_REQUEST, 'ms3_cart_change_options_error'); } + public function testChangeOptionJsonStringOptionsReturns200(): void + { + $add = $this->dispatch('POST', '/api/v1/cart/add', [], [ + 'id' => JourneyProductCatalog::FIXTURE_PRODUCT_ID, + 'count' => 1, + 'options' => ['size' => 'M'], + ]); + $token = $this->lastMs3Token(); + $key = (string) ($add['data']['last_key'] ?? ''); + + $res = $this->dispatch( + 'POST', + '/api/v1/cart/change-option', + [], + ['product_key' => $key, 'options' => '{"size":"L"}'], + [], + $token + ); + + self::assertSame(HttpStatus::OK, $res['status']); + self::assertTrue($res['success']); + $newKey = (string) ($res['data']['last_key'] ?? $key); + self::assertSame('L', $res['data']['cart'][$newKey]['options']['size'] ?? null); + } + + public function testChangeOptionInvalidOptionsTypeReturns400(): void + { + $add = $this->dispatch('POST', '/api/v1/cart/add', [], [ + 'id' => JourneyProductCatalog::FIXTURE_PRODUCT_ID, + 'count' => 1, + 'options' => ['size' => 'M'], + ]); + $token = $this->lastMs3Token(); + $key = (string) ($add['data']['last_key'] ?? ''); + + $res = $this->dispatch( + 'POST', + '/api/v1/cart/change-option', + [], + ['product_key' => $key, 'options' => 42], + [], + $token + ); + $this->assertApiError($res, HttpStatus::BAD_REQUEST, 'ms3_err_cart_options'); + } + public function testSubmitEmptyCartReturnsBusinessError(): void { $token = $this->modx->journeyTokens->generateCustomerToken()['token']; diff --git a/core/components/minishop3/tests/Unit/Services/Cart/CartItemManagerPureTest.php b/core/components/minishop3/tests/Unit/Services/Cart/CartItemManagerPureTest.php index f3e7ad709..6e9f6459b 100644 --- a/core/components/minishop3/tests/Unit/Services/Cart/CartItemManagerPureTest.php +++ b/core/components/minishop3/tests/Unit/Services/Cart/CartItemManagerPureTest.php @@ -58,10 +58,10 @@ public static function countCases(): iterable public function testNormalizeOptions(): void { - self::assertSame(['color' => 'red'], $this->manager->normalizeOptions('{"color":"red"}')); - self::assertSame(['a' => 1], $this->manager->normalizeOptions(['a' => 1])); - self::assertSame([], $this->manager->normalizeOptions('{bad')); - self::assertSame([], $this->manager->normalizeOptions(null)); + self::assertSame(['color' => 'red'], CartItemManager::normalizeOptions('{"color":"red"}')); + self::assertSame(['a' => 1], CartItemManager::normalizeOptions(['a' => 1])); + self::assertSame([], CartItemManager::normalizeOptions('{bad')); + self::assertSame([], CartItemManager::normalizeOptions(null)); } public function testGenerateProductKeyDiffersByOptions(): void