Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

## Навигация

- **Текущий месяц:** [Апрель 2026](#апрель-2026) (ниже)
- **Предыдущий месяц:** [Март 2026](#март-2026) (ниже)
- **Текущий месяц:** [Май 2026](#май-2026) (ниже)
- **Предыдущий месяц:** [Апрель 2026](#апрель-2026) (ниже)
- **Ещё раньше:** [Февраль 2026](#февраль-2026), [Январь 2026](#январь-2026) (ниже)
- **Архив по месяцам:**
- [Декабрь 2025](changelogs/2025-12.md)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion assets/components/minishop3/js/web/core/CustomerAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object>}
*
Expand Down
16 changes: 16 additions & 0 deletions core/components/minishop3/config/routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions core/components/minishop3/lexicon/en/customer.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions core/components/minishop3/lexicon/ru/customer.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 успешно подтвержден';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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);
Expand All @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down