diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f03ef0c..494d1477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ ## Навигация -- **Текущий месяц:** [Апрель 2026](#апрель-2026) (ниже) -- **Предыдущий месяц:** [Март 2026](#март-2026) (ниже) +- **Текущий месяц:** [Май 2026](#май-2026) (ниже) +- **Предыдущий месяц:** [Апрель 2026](#апрель-2026) (ниже) - **Ещё раньше:** [Февраль 2026](#февраль-2026), [Январь 2026](#январь-2026) (ниже) - **Архив по месяцам:** - [Декабрь 2025](changelogs/2025-12.md) @@ -15,6 +15,19 @@ --- +## Май 2026 + +### Разработка + +#### 🐛 Исправлено + +**Web API покупателя — отсутствующие маршруты CustomerAPI (#241):** +- Добавлен `POST /api/v1/customer/add` для быстрого обновления полей профиля (`first_name`, `last_name`, `email`, `phone`) через существующий `CustomerAPI.add()` и `CustomerUI.handleAdd()`. +- Добавлен `POST /api/v1/customer/changeAddress` как совместимый endpoint для выбора сохранённого адреса в черновике заказа по `address_hash`. +- Для быстрого обновления профиля добавлена whitelist-валидация и отдельное сообщение lexicon на `ru/en`; проверка уникальности email и сброс `email_verified_at` вынесены в общие методы контроллера профиля. + +--- + ## Апрель 2026 ### [2026-04-27] 🚀 Версия 1.10.1-beta1 diff --git a/assets/components/minishop3/js/web/core/CustomerAPI.js b/assets/components/minishop3/js/web/core/CustomerAPI.js index cc9501f7..f137807f 100644 --- a/assets/components/minishop3/js/web/core/CustomerAPI.js +++ b/assets/components/minishop3/js/web/core/CustomerAPI.js @@ -21,7 +21,7 @@ class CustomerAPI { * * POST /api/v1/customer/add * - * @param {string} key - Field key (email, phone, fullname, etc.) + * @param {string} key - Field key (first_name, last_name, email, phone) * @param {string} value - Field value * @returns {Promise} * diff --git a/core/components/minishop3/config/routes/web.php b/core/components/minishop3/config/routes/web.php index e377e5bd..ff97e742 100644 --- a/core/components/minishop3/config/routes/web.php +++ b/core/components/minishop3/config/routes/web.php @@ -183,6 +183,16 @@ return Response::success($response->getObject(), $response->getMessage()); }); + + $router->post('/add', function($params) use ($modx) { + $ms3 = $modx->services->get('ms3'); + $input = file_get_contents('php://input'); + $data = json_decode($input, true) ?: []; + + $controller = new \MiniShop3\Controllers\Api\Web\CustomerProfileController($modx, $ms3); + return $controller->updateField($data); + }, [$tokenMiddleware]); + $router->get('/token/get', function($params) use ($modx) { $ms3 = $modx->services->get('ms3'); $ms3->initialize(); @@ -245,6 +255,12 @@ $controller = new \MiniShop3\Controllers\Api\Web\CustomerProfileController($modx, $ms3); return $controller->update($data); }, [$tokenMiddleware]); + + $router->post('/changeAddress', function($params) use ($modx) { + $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); + return $controller->changeCustomerAddress($params); + }, [$tokenMiddleware]); + $router->post('/email/resend-verification', function($params) use ($modx) { $ms3 = $modx->services->get('ms3'); $controller = new \MiniShop3\Controllers\Api\Web\CustomerEmailController($modx, $ms3); diff --git a/core/components/minishop3/lexicon/en/customer.inc.php b/core/components/minishop3/lexicon/en/customer.inc.php index 5cb9e519..4bf69e7e 100644 --- a/core/components/minishop3/lexicon/en/customer.inc.php +++ b/core/components/minishop3/lexicon/en/customer.inc.php @@ -60,6 +60,7 @@ $_lang['ms3_customer_err_token_create'] = 'Error creating token'; $_lang['ms3_customer_err_save'] = 'Error saving data'; $_lang['ms3_customer_err_register_rate_limit'] = 'Registration limit exceeded. Please try again later.'; +$_lang['ms3_customer_err_field_not_allowed'] = 'This field cannot be changed through the quick profile endpoint'; // Email Verification $_lang['ms3_customer_email_verified'] = 'Email successfully verified'; diff --git a/core/components/minishop3/lexicon/ru/customer.inc.php b/core/components/minishop3/lexicon/ru/customer.inc.php index 938310c1..bcc67993 100644 --- a/core/components/minishop3/lexicon/ru/customer.inc.php +++ b/core/components/minishop3/lexicon/ru/customer.inc.php @@ -60,6 +60,7 @@ $_lang['ms3_customer_err_token_create'] = 'Ошибка создания токена'; $_lang['ms3_customer_err_save'] = 'Ошибка сохранения данных'; $_lang['ms3_customer_err_register_rate_limit'] = 'Превышен лимит регистраций. Попробуйте позже.'; +$_lang['ms3_customer_err_field_not_allowed'] = 'Это поле нельзя изменить через быстрый профиль'; // Email Verification $_lang['ms3_customer_email_verified'] = 'Email успешно подтвержден'; diff --git a/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php b/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php index 0480ccd9..c957200d 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php @@ -50,22 +50,16 @@ public function update(array $data): array return $this->error($this->modx->lexicon('ms3_customer_err_login_required')); } - $customerId = (int)$_SESSION['ms3']['customer_id']; - /** @var msCustomer $customer */ - $customer = $this->modx->getObject(msCustomer::class, $customerId); + $customer = $this->getCurrentCustomer(); if (!$customer) { return $this->error($this->modx->lexicon('ms3_err_customer_nf')); } + $customerId = (int)$customer->get('id'); $validator = new Validator(); - $validation = $validator->make($data, [ - 'first_name' => 'required|min:2|max:100', - 'last_name' => 'required|min:2|max:100', - 'email' => 'required|email', - 'phone' => 'required|min:10|max:20', - ]); + $validation = $validator->make($data, $this->getProfileFieldRules()); $validation->validate(); @@ -79,30 +73,17 @@ public function update(array $data): array ); } - $oldEmail = $customer->get('email'); $newEmail = trim($data['email']); - if ($oldEmail !== $newEmail) { - $existingCustomer = $this->modx->getObject(msCustomer::class, [ - 'email' => $newEmail, - 'id:!=' => $customerId, - ]); - - if ($existingCustomer) { - $_SESSION['ms3']['customer_profile_errors'] = [ - 'email' => $this->modx->lexicon('ms3_customer_err_email_exists') - ]; - return $this->error($this->modx->lexicon('ms3_customer_err_email_exists')); - } - - $customer->set('email_verified_at', null); - - $this->modx->log( - modX::LOG_LEVEL_INFO, - "[CustomerProfileController] Email changed for customer #{$customerId}: {$oldEmail} → {$newEmail}. Verification reset." - ); + if (!$this->isEmailAvailable($customer, $newEmail)) { + $_SESSION['ms3']['customer_profile_errors'] = [ + 'email' => $this->modx->lexicon('ms3_customer_err_email_exists') + ]; + return $this->error($this->modx->lexicon('ms3_customer_err_email_exists')); } + $this->resetEmailVerificationIfChanged($customer, $newEmail); + $customer->set('first_name', trim($data['first_name'])); $customer->set('last_name', trim($data['last_name'])); $customer->set('email', $newEmail); @@ -125,6 +106,119 @@ public function update(array $data): array ); } + /** + * Update a single customer profile field. + * + * POST /api/v1/customer/add + * + * @param array $data Request data with key and value + * @return array ['success' => bool, 'message' => string, 'data' => array] + */ + public function updateField(array $data): array + { + if (empty($_SESSION['ms3']['customer_id'])) { + return $this->error($this->modx->lexicon('ms3_customer_err_login_required')); + } + + $customer = $this->getCurrentCustomer(); + if (!$customer) { + return $this->error($this->modx->lexicon('ms3_err_customer_nf')); + } + + $key = trim((string)($data['key'] ?? '')); + if ($key === '') { + return $this->error($this->modx->lexicon('ms3_customer_key_empty')); + } + + $rules = $this->getProfileFieldRules(); + if (!isset($rules[$key])) { + return $this->error($this->modx->lexicon('ms3_customer_err_field_not_allowed')); + } + + $value = trim((string)($data['value'] ?? '')); + $validation = (new Validator())->make([$key => $value], [$key => $rules[$key]]); + $validation->validate(); + + if ($validation->fails()) { + $errors = $validation->errors()->firstOfAll(); + + return $this->error( + $this->modx->lexicon('ms3_customer_err_validation'), + ['errors' => $errors] + ); + } + + if ($key === 'email' && !$this->isEmailAvailable($customer, $value)) { + return $this->error($this->modx->lexicon('ms3_customer_err_email_exists')); + } + + if ($key === 'email') { + $this->resetEmailVerificationIfChanged($customer, $value); + } + + $customer->set($key, $value); + + if (!$customer->save()) { + return $this->error($this->modx->lexicon('ms3_customer_err_save')); + } + + return $this->success( + $this->modx->lexicon('ms3_customer_profile_updated'), + [ + $key => $customer->get($key), + 'customer' => $customer->toArray(), + ] + ); + } + + protected function getCurrentCustomer(): ?msCustomer + { + if (empty($_SESSION['ms3']['customer_id'])) { + return null; + } + + $customer = $this->modx->getObject(msCustomer::class, (int)$_SESSION['ms3']['customer_id']); + + return $customer instanceof msCustomer ? $customer : null; + } + + protected function getProfileFieldRules(): array + { + return [ + 'first_name' => 'required|min:2|max:100', + 'last_name' => 'required|min:2|max:100', + 'email' => 'required|email', + 'phone' => 'required|min:10|max:20', + ]; + } + + protected function isEmailAvailable(msCustomer $customer, string $email): bool + { + if ((string)$customer->get('email') === $email) { + return true; + } + + return !$this->modx->getObject(msCustomer::class, [ + 'email' => $email, + 'id:!=' => $customer->get('id'), + ]); + } + + protected function resetEmailVerificationIfChanged(msCustomer $customer, string $email): void + { + $oldEmail = (string)$customer->get('email'); + if ($oldEmail === $email) { + return; + } + + $customer->set('email_verified_at', null); + + $this->modx->log( + modX::LOG_LEVEL_INFO, + "[CustomerProfileController] Email changed for customer #{$customer->get('id')}: {$oldEmail} → {$email}. Verification reset." + ); + } + /** * Success response * diff --git a/core/components/minishop3/src/Controllers/Api/Web/OrderController.php b/core/components/minishop3/src/Controllers/Api/Web/OrderController.php index 43ec6ab7..fbeb6043 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/OrderController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/OrderController.php @@ -303,6 +303,26 @@ public function setCustomerAddress(array $params = []): array $input = $this->getRequestData(); $addressHash = $input['address_hash'] ?? null; + return $this->setCustomerAddressByHash($addressHash); + } + + /** + * Set customer address from CustomerAPI legacy payload. + * POST /api/v1/customer/changeAddress + * + * @param array $params URL parameters + * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + */ + public function changeCustomerAddress(array $params = []): array + { + $input = $this->getRequestData(); + + $addressHash = $input['address_hash'] ?? ($input['value'] ?? null); + return $this->setCustomerAddressByHash($addressHash); + } + + protected function setCustomerAddressByHash(?string $addressHash = null): array + { $token = $_REQUEST['ms3_token'] ?? ''; if (empty($token)) {