From 8f02bc350e92498fa6cbe0873e04beef0609fd35 Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Sun, 16 Aug 2026 15:10:51 +0600 Subject: [PATCH 1/5] =?UTF-8?q?feat(web-api):=20=D0=B5=D0=B4=D0=B8=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA=20=D0=B8=20HTTP-=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сохраняем envelope #341 и добавляем machine error_code, чтобы Nuxt получал стабильные 401/404/422/429 и field errors без getData()-потери status. --- .../Controllers/Api/Web/CartController.php | 67 +++---- .../Api/Web/CustomerAddressController.php | 114 ++++++------ .../Api/Web/CustomerEmailController.php | 71 +++---- .../Api/Web/CustomerOrderController.php | 42 +++-- .../Api/Web/CustomerProfileController.php | 64 +++---- .../Controllers/Api/Web/OrderController.php | 105 +++++------ .../src/Middleware/RateLimitMiddleware.php | 7 +- .../src/Middleware/TokenMiddleware.php | 19 +- .../minishop3/src/Router/ApiErrorCode.php | 42 +++++ .../src/Router/DomainMs2Response.php | 120 ++++++++++++ .../minishop3/src/Router/Response.php | 91 ++++++--- .../tests/ResponseFromProcessorTest.php | 10 + .../tests/TokenMiddlewarePublicRoutesTest.php | 30 ++- .../tests/WebApiErrorContractTest.php | 174 ++++++++++++++++++ 14 files changed, 700 insertions(+), 256 deletions(-) create mode 100644 core/components/minishop3/src/Router/ApiErrorCode.php create mode 100644 core/components/minishop3/src/Router/DomainMs2Response.php create mode 100644 core/components/minishop3/tests/WebApiErrorContractTest.php diff --git a/core/components/minishop3/src/Controllers/Api/Web/CartController.php b/core/components/minishop3/src/Controllers/Api/Web/CartController.php index 6c6a26b4..74c1b185 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CartController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CartController.php @@ -2,6 +2,8 @@ namespace MiniShop3\Controllers\Api\Web; +use MiniShop3\Router\ApiErrorCode; +use MiniShop3\Router\DomainMs2Response; use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; use MiniShop3\Services\Api\WebApiContextResolver; @@ -30,9 +32,9 @@ public function __construct(modX $modx) * POST /api/v1/cart/add * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function add(array $params = []): array + public function add(array $params = []): Response { $input = $this->getRequestData(); @@ -59,9 +61,9 @@ public function add(array $params = []): array * POST /api/v1/cart/change * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function change(array $params = []): array + public function change(array $params = []): Response { $input = $this->getRequestData(); @@ -77,7 +79,7 @@ public function change(array $params = []): array return Response::error( $this->modx->lexicon('ms3_err_product_key_required'), HttpStatus::BAD_REQUEST - )->getData(); + ); } $ms3 = $this->modx->services->get('ms3'); @@ -94,9 +96,9 @@ public function change(array $params = []): array * POST /api/v1/cart/change-option * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function changeOption(array $params = []): array + public function changeOption(array $params = []): Response { $input = $this->getRequestData(); @@ -112,14 +114,14 @@ public function changeOption(array $params = []): array return Response::error( $this->modx->lexicon('ms3_err_product_key_required'), HttpStatus::BAD_REQUEST - )->getData(); + ); } if (!is_array($options) || $options === []) { return Response::error( $this->modx->lexicon('ms3_cart_change_options_error'), HttpStatus::BAD_REQUEST - )->getData(); + ); } $ms3 = $this->modx->services->get('ms3'); @@ -136,9 +138,9 @@ public function changeOption(array $params = []): array * POST /api/v1/cart/remove * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function remove(array $params = []): array + public function remove(array $params = []): Response { $input = $this->getRequestData(); @@ -153,7 +155,7 @@ public function remove(array $params = []): array return Response::error( $this->modx->lexicon('ms3_err_product_key_required'), HttpStatus::BAD_REQUEST - )->getData(); + ); } $ms3 = $this->modx->services->get('ms3'); @@ -170,9 +172,9 @@ public function remove(array $params = []): array * GET /api/v1/cart/get * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function get(array $params = []): array + public function get(array $params = []): Response { $token = $_REQUEST['ms3_token'] ?? ''; @@ -194,9 +196,9 @@ public function get(array $params = []): array * POST /api/v1/cart/clean * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function clean(array $params = []): array + public function clean(array $params = []): Response { $token = $_REQUEST['ms3_token'] ?? ''; @@ -214,14 +216,15 @@ public function clean(array $params = []): array } /** - * @return array{success: bool, message: string, code: int, errors: mixed} + * @return Response */ - private function tokenRequiredError(): array + private function tokenRequiredError(): Response { - return Response::error( + return Response::errorWithCode( + ApiErrorCode::TOKEN_REQUIRED, $this->modx->lexicon('ms3_customer_err_token_required'), HttpStatus::UNAUTHORIZED - )->getData(); + ); } /** @@ -251,17 +254,16 @@ protected function getRequestData(): array } /** - * Transform Cart response to API format + * Transform Cart domain MS2-array to Web API Response (#572). * - * @param array $result Response from Cart controller - * @return array Response in API format ['success' => bool, 'message' => '', 'data' => [...]] + * @param array $result */ - protected function transformResponse(array $result): array + protected function transformResponse(array $result): Response { $input = $this->getRequestData(); $renderTokens = $input['render'] ?? null; - if (!empty($renderTokens) && $result['success']) { + if (!empty($renderTokens) && !empty($result['success'])) { $customerToken = $_REQUEST['ms3_token'] ?? ''; $renderedHtml = $this->renderSnippets($renderTokens, $customerToken); @@ -270,15 +272,14 @@ protected function transformResponse(array $result): array } } - if ($result['success']) { - return Response::success($result['data'], $result['message'] ?? '')->getData(); - } else { - return Response::error( - $result['message'] ?? $this->modx->lexicon('ms3_err_unknown'), - HttpStatus::BAD_REQUEST, - $result['data'] ?? [] - )->getData(); + if (!empty($result['success'])) { + return Response::success($result['data'] ?? null, $result['message'] ?? ''); } + + return DomainMs2Response::failure( + (string) ($result['message'] ?? $this->modx->lexicon('ms3_err_unknown')), + $result['data'] ?? null, + ); } /** diff --git a/core/components/minishop3/src/Controllers/Api/Web/CustomerAddressController.php b/core/components/minishop3/src/Controllers/Api/Web/CustomerAddressController.php index 8ced4ab4..1a5bb253 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CustomerAddressController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CustomerAddressController.php @@ -4,6 +4,7 @@ use MiniShop3\Model\msCustomer; use MiniShop3\Model\msCustomerAddress; +use MiniShop3\Router\ApiErrorCode; use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; use MODX\Revolution\modX; @@ -33,14 +34,14 @@ public function __construct(modX $modx) * GET /api/v1/customer/addresses * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function getList(array $params = []): array + public function getList(array $params = []): Response { $customer = $this->getAuthorizedCustomer(); if (!$customer) { - return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED); } $addresses = $this->modx->getIterator(msCustomerAddress::class, [ @@ -53,7 +54,10 @@ public function getList(array $params = []): array $data[] = $this->formatAddress($address); } - return Response::success($data)->getData(); + return Response::success([ + 'items' => $data, + 'total' => count($data), + ]); } /** @@ -61,20 +65,20 @@ public function getList(array $params = []): array * GET /api/v1/customer/addresses/{id} * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function get(array $params = []): array + public function get(array $params = []): Response { $customer = $this->getAuthorizedCustomer(); if (!$customer) { - return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED); } $addressId = (int)($params['id'] ?? 0); if (!$addressId) { - return Response::error($this->modx->lexicon('ms3_customer_err_address_id_not_specified'), HttpStatus::BAD_REQUEST)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_address_id_not_specified'), HttpStatus::BAD_REQUEST); } $address = $this->modx->getObject(msCustomerAddress::class, [ @@ -83,10 +87,10 @@ public function get(array $params = []): array ]); if (!$address) { - return Response::error($this->modx->lexicon('ms3_customer_err_address_not_found'), HttpStatus::NOT_FOUND)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_address_not_found'), HttpStatus::NOT_FOUND); } - return Response::success($this->formatAddress($address))->getData(); + return Response::success($this->formatAddress($address)); } /** @@ -94,24 +98,33 @@ public function get(array $params = []): array * POST /api/v1/customer/addresses * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function create(array $params = []): array + public function create(array $params = []): Response { $customer = $this->getAuthorizedCustomer(); if (!$customer) { - return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED); } $input = $this->getRequestData(); $required = ['name', 'city', 'street']; + $missing = []; foreach ($required as $field) { if (empty($input[$field])) { - return Response::error($this->modx->lexicon('ms3_customer_err_field_required'), HttpStatus::BAD_REQUEST)->getData(); + $missing[$field] = $this->modx->lexicon('ms3_customer_err_field_required'); } } + if ($missing !== []) { + return Response::errorWithCode( + ApiErrorCode::VALIDATION_FAILED, + $this->modx->lexicon('ms3_customer_err_field_required'), + HttpStatus::UNPROCESSABLE_ENTITY, + $missing, + ); + } $addressHash = $this->generateAddressHash($input); $exists = $this->modx->getObject(msCustomerAddress::class, [ @@ -120,9 +133,12 @@ public function create(array $params = []): array ]); if ($exists) { - return Response::error($this->modx->lexicon('ms3_customer_address_already_exists'), HttpStatus::CONFLICT, [ - 'existing_id' => $exists->get('id') - ])->getData(); + return Response::errorWithCode( + ApiErrorCode::CONFLICT, + $this->modx->lexicon('ms3_customer_address_already_exists'), + HttpStatus::CONFLICT, + data: ['existing_id' => $exists->get('id')], + ); } $address = $this->modx->newObject(msCustomerAddress::class); @@ -138,12 +154,12 @@ public function create(array $params = []): array } if (!$address->save()) { - return Response::error($this->modx->lexicon('ms3_customer_address_creation_error'), HttpStatus::INTERNAL_SERVER_ERROR)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_address_creation_error'), HttpStatus::INTERNAL_SERVER_ERROR); } $this->modx->log(modX::LOG_LEVEL_INFO, '[MS3] Created customer address: ' . $addressHash . ' for customer #' . $customer->get('id')); - return Response::success($this->formatAddress($address), $this->modx->lexicon('ms3_customer_address_added'), HttpStatus::CREATED)->getData(); + return Response::success($this->formatAddress($address), $this->modx->lexicon('ms3_customer_address_added'), HttpStatus::CREATED); } /** @@ -151,20 +167,20 @@ public function create(array $params = []): array * PUT /api/v1/customer/addresses/{id} * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function update(array $params = []): array + public function update(array $params = []): Response { $customer = $this->getAuthorizedCustomer(); if (!$customer) { - return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED); } $addressId = (int)($params['id'] ?? 0); if (!$addressId) { - return Response::error($this->modx->lexicon('ms3_customer_err_address_id_not_specified'), HttpStatus::BAD_REQUEST)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_address_id_not_specified'), HttpStatus::BAD_REQUEST); } $address = $this->modx->getObject(msCustomerAddress::class, [ @@ -173,7 +189,7 @@ public function update(array $params = []): array ]); if (!$address) { - return Response::error($this->modx->lexicon('ms3_customer_err_address_not_found'), HttpStatus::NOT_FOUND)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_address_not_found'), HttpStatus::NOT_FOUND); } $input = $this->getRequestData(); @@ -194,13 +210,13 @@ public function update(array $params = []): array $address->set('updatedon', date('Y-m-d H:i:s')); if (!$address->save()) { - return Response::error($this->modx->lexicon('ms3_customer_address_update_error'), HttpStatus::INTERNAL_SERVER_ERROR)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_address_update_error'), HttpStatus::INTERNAL_SERVER_ERROR); } $this->modx->log(modX::LOG_LEVEL_INFO, '[MS3] Updated customer address #' . $addressId); } - return Response::success($this->formatAddress($address), $this->modx->lexicon('ms3_customer_address_updated'))->getData(); + return Response::success($this->formatAddress($address), $this->modx->lexicon('ms3_customer_address_updated')); } /** @@ -208,20 +224,20 @@ public function update(array $params = []): array * PUT /api/v1/customer/addresses/{id}/set-default * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function setDefault(array $params = []): array + public function setDefault(array $params = []): Response { $customer = $this->getAuthorizedCustomer(); if (!$customer) { - return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED); } $addressId = (int)($params['id'] ?? 0); if (!$addressId) { - return Response::error($this->modx->lexicon('ms3_customer_err_address_id_not_specified'), HttpStatus::BAD_REQUEST)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_address_id_not_specified'), HttpStatus::BAD_REQUEST); } $address = $this->modx->getObject(msCustomerAddress::class, [ @@ -231,7 +247,7 @@ public function setDefault(array $params = []): array ]); if (!$address) { - return Response::error($this->modx->lexicon('ms3_customer_err_address_not_found'), HttpStatus::NOT_FOUND)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_address_not_found'), HttpStatus::NOT_FOUND); } $table = $this->modx->getTableName(msCustomerAddress::class); @@ -243,12 +259,12 @@ public function setDefault(array $params = []): array $address->set('updatedon', date('Y-m-d H:i:s')); if (!$address->save()) { - return Response::error($this->modx->lexicon('ms3_customer_address_default_error'), HttpStatus::INTERNAL_SERVER_ERROR)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_address_default_error'), HttpStatus::INTERNAL_SERVER_ERROR); } $this->modx->log(modX::LOG_LEVEL_INFO, '[MS3] Set default address #' . $addressId . ' for customer #' . $customer->get('id')); - return Response::success($this->formatAddress($address), $this->modx->lexicon('ms3_customer_address_default_set'))->getData(); + return Response::success($this->formatAddress($address), $this->modx->lexicon('ms3_customer_address_default_set')); } /** @@ -256,20 +272,20 @@ public function setDefault(array $params = []): array * DELETE /api/v1/customer/addresses/{id} * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function delete(array $params = []): array + public function delete(array $params = []): Response { $customer = $this->getAuthorizedCustomer(); if (!$customer) { - return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_not_authorized'), HttpStatus::UNAUTHORIZED); } $addressId = (int)($params['id'] ?? 0); if (!$addressId) { - return Response::error($this->modx->lexicon('ms3_customer_err_address_id_not_specified'), HttpStatus::BAD_REQUEST)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_address_id_not_specified'), HttpStatus::BAD_REQUEST); } $address = $this->modx->getObject(msCustomerAddress::class, [ @@ -278,26 +294,25 @@ public function delete(array $params = []): array ]); if (!$address) { - return Response::error($this->modx->lexicon('ms3_customer_err_address_not_found'), HttpStatus::NOT_FOUND)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_err_address_not_found'), HttpStatus::NOT_FOUND); } $address->set('active', 0); $address->set('updatedon', date('Y-m-d H:i:s')); if (!$address->save()) { - return Response::error($this->modx->lexicon('ms3_customer_address_delete_error'), HttpStatus::INTERNAL_SERVER_ERROR)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_address_delete_error'), HttpStatus::INTERNAL_SERVER_ERROR); } $this->modx->log(modX::LOG_LEVEL_INFO, '[MS3] Deleted customer address #' . $addressId); - return Response::success(null, $this->modx->lexicon('ms3_customer_address_deleted'))->getData(); + return Response::success(null, $this->modx->lexicon('ms3_customer_address_deleted')); } /** * Format address for API response * - * @param msCustomerAddress $address - * @return array + * @return array */ protected function formatAddress(msCustomerAddress $address): array { @@ -353,19 +368,4 @@ protected function getRequestData(): array return is_array($data) ? $data : []; } - - /** - * Transform response from old format to new - * - * @param array $result - * @return array - */ - protected function transformResponse(array $result): array - { - if (isset($result['success'])) { - return $result; - } - - return Response::success($result)->getData(); - } } diff --git a/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php b/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php index 3151e61d..782587cc 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php @@ -4,6 +4,7 @@ use MiniShop3\MiniShop3; use MiniShop3\Model\msCustomer; +use MiniShop3\Router\ApiErrorCode; use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; use MiniShop3\Services\Customer\AuthManager; @@ -51,15 +52,15 @@ public function __construct(modX $modx, MiniShop3 $ms3) * * POST /api/v1/customer/email/resend-verification * - * @return array ['success' => bool, 'message' => string] + * @return Response ['success' => bool, 'message' => string] */ - public function resendVerification(): array + public function resendVerification(): Response { if (empty($_SESSION['ms3']['customer_id'])) { return Response::error( $this->modx->lexicon('ms3_customer_err_login_required'), HttpStatus::UNAUTHORIZED - )->getData(); + ); } $customerId = (int)$_SESSION['ms3']['customer_id']; @@ -71,23 +72,27 @@ public function resendVerification(): array return Response::error( $this->modx->lexicon('ms3_err_customer_nf'), HttpStatus::UNAUTHORIZED - )->getData(); + ); } $result = $this->emailVerification->resendVerificationEmail($customer); - if ($result['success']) { + if (!empty($result['success'])) { $this->modx->log( modX::LOG_LEVEL_INFO, "[CustomerEmailController] Verification email resent to customer #{$customerId}" ); - return $result; + + return Response::success( + $result['data'] ?? null, + $result['message'] ?? '' + ); } return Response::error( - $result['message'], + (string) ($result['message'] ?? $this->modx->lexicon('ms3_err_unknown')), Response::statusFromProcessorObject($result) - )->getData(); + ); } /** @@ -99,9 +104,9 @@ public function resendVerification(): array * - `html=1` (как в ссылке из письма) — после успеха/ошибки HTTP 302 на сайт (см. GH-226). * * @param array $params Request parameters - * @return array|Response + * @return Response */ - public function verify(array $params): array|Response + public function verify(array $params): Response { $formatJson = ($params['format'] ?? '') === 'json'; $htmlFlow = ($params['html'] ?? '') === '1'; @@ -113,7 +118,11 @@ public function verify(array $params): array|Response return Response::redirect($this->buildEmailVerificationFailedRedirectUrl(), 302); } - return $this->error($this->modx->lexicon('ms3_customer_err_token_required')); + return Response::errorWithCode( + ApiErrorCode::BAD_REQUEST, + $this->modx->lexicon('ms3_customer_err_token_required'), + HttpStatus::BAD_REQUEST + ); } $customer = $this->emailVerification->verifyToken($token); @@ -123,7 +132,11 @@ public function verify(array $params): array|Response return Response::redirect($this->buildEmailVerificationFailedRedirectUrl(), 302); } - return $this->error($this->modx->lexicon('ms3_customer_err_email_verification_invalid')); + return Response::errorWithCode( + ApiErrorCode::BAD_REQUEST, + $this->modx->lexicon('ms3_customer_err_email_verification_invalid'), + HttpStatus::BAD_REQUEST + ); } /** @var AuthManager $authManager */ @@ -197,34 +210,24 @@ protected function buildEmailVerificationFailedRedirectUrl(): string } /** - * Success response - * - * @param string $message - * @param array $data - * @return array + * @param array $data */ - protected function success(string $message = '', array $data = []): array + protected function success(string $message = '', array $data = []): Response { - return [ - 'success' => true, - 'message' => $message, - 'data' => $data, - ]; + return Response::success($data, $message); } /** - * Error response - * - * @param string $message - * @param array $data - * @return array + * @param array $data */ - protected function error(string $message, array $data = []): array + protected function error(string $message, array $data = []): Response { - return [ - 'success' => false, - 'message' => $message, - 'data' => $data, - ]; + return Response::error( + $message, + HttpStatus::BAD_REQUEST, + null, + null, + $data !== [] ? $data : null, + ); } } diff --git a/core/components/minishop3/src/Controllers/Api/Web/CustomerOrderController.php b/core/components/minishop3/src/Controllers/Api/Web/CustomerOrderController.php index 2a03c798..04ee5e89 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CustomerOrderController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CustomerOrderController.php @@ -31,26 +31,30 @@ public function __construct(modX $modx) * GET /api/v1/customer/orders?limit=&offset=&status= * * @param array $params URL parameters - * @return array + * @return Response */ - public function getList(array $params = []): array + public function getList(array $params = []): Response { $customer = $this->getAuthorizedCustomer(); if (!$customer) { - return Response::error($this->modx->lexicon('ms3_customer_order_err_unauthorized'), HttpStatus::UNAUTHORIZED)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_order_err_unauthorized'), HttpStatus::UNAUTHORIZED); } /** @var CustomerOrderService $service */ $service = $this->modx->services->get('ms3_customer_order'); $query = CustomerOrderService::normalizeListParams($_GET, $service->getDraftStatusId()); - return Response::success($service->listForCustomer( + $payload = $service->listForCustomer( (int)$customer->get('id'), $query['limit'], $query['offset'], $query['status_id'] - ))->getData(); + ); + // Additive alias for pagination convention (#572); keep `orders` for BC. + $payload['items'] = $payload['orders'] ?? []; + + return Response::success($payload); } /** @@ -58,20 +62,20 @@ public function getList(array $params = []): array * GET /api/v1/customer/orders/{id} * * @param array $params URL parameters (id = order ID) - * @return array + * @return Response */ - public function get(array $params = []): array + public function get(array $params = []): Response { $customer = $this->getAuthorizedCustomer(); if (!$customer) { - return Response::error($this->modx->lexicon('ms3_customer_order_err_unauthorized'), HttpStatus::UNAUTHORIZED)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_order_err_unauthorized'), HttpStatus::UNAUTHORIZED); } $orderId = (int)($params['id'] ?? 0); if ($orderId < 1) { - return Response::error($this->modx->lexicon('ms3_customer_order_err_no_id'), HttpStatus::BAD_REQUEST)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_order_err_no_id'), HttpStatus::BAD_REQUEST); } /** @var CustomerOrderService $service */ @@ -79,10 +83,10 @@ public function get(array $params = []): array $detail = $service->getForCustomer((int)$customer->get('id'), $orderId); if ($detail === null) { - return Response::error($this->modx->lexicon('ms3_customer_order_err_not_found'), HttpStatus::NOT_FOUND)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_order_err_not_found'), HttpStatus::NOT_FOUND); } - return Response::success($detail)->getData(); + return Response::success($detail); } /** @@ -90,20 +94,20 @@ public function get(array $params = []): array * POST /api/v1/customer/orders/{id}/cancel * * @param array $params URL parameters (id = order ID) - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function cancel(array $params = []): array + public function cancel(array $params = []): Response { $customer = $this->getAuthorizedCustomer(); if (!$customer) { - return Response::error($this->modx->lexicon('ms3_customer_order_cancel_err_unauthorized'), HttpStatus::UNAUTHORIZED)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_order_cancel_err_unauthorized'), HttpStatus::UNAUTHORIZED); } $orderId = (int)($params['id'] ?? 0); if ($orderId < 1) { - return Response::error($this->modx->lexicon('ms3_customer_order_cancel_err_no_order'), HttpStatus::BAD_REQUEST)->getData(); + return Response::error($this->modx->lexicon('ms3_customer_order_cancel_err_no_order'), HttpStatus::BAD_REQUEST); } /** @var CustomerOrderService $service */ @@ -115,21 +119,21 @@ public function cancel(array $params = []): array 'not_found' => Response::error( $this->modx->lexicon('ms3_customer_order_cancel_err_not_found'), HttpStatus::NOT_FOUND - )->getData(), + ), 'status' => Response::error( $this->modx->lexicon('ms3_customer_order_cancel_err_status'), HttpStatus::BAD_REQUEST - )->getData(), + ), default => Response::error( $result['message'] ?? $this->modx->lexicon('ms3_customer_order_cancel_err_failed'), HttpStatus::BAD_REQUEST - )->getData(), + ), }; } return Response::success( ['order_id' => $result['order_id'], 'status_id' => $result['status_id']], $this->modx->lexicon('ms3_customer_order_cancelled') - )->getData(); + ); } } diff --git a/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php b/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php index e95b8937..bd428404 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php @@ -4,6 +4,7 @@ use MiniShop3\MiniShop3; use MiniShop3\Model\msCustomer; +use MiniShop3\Router\ApiErrorCode; use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; use MiniShop3\Services\Customer\CustomerPublicDto; @@ -45,15 +46,15 @@ public function __construct(modX $modx, MiniShop3 $ms3) * PUT /api/v1/customer/profile * * @param array $data Form data - * @return array ['success' => bool, 'message' => string, 'data' => array] + * @return Response ['success' => bool, 'message' => string, 'data' => array] */ - public function update(array $data): array + public function update(array $data): Response { if (empty($_SESSION['ms3']['customer_id'])) { return Response::error( $this->modx->lexicon('ms3_customer_err_login_required'), HttpStatus::UNAUTHORIZED - )->getData(); + ); } /** @var msCustomer $customer */ @@ -63,7 +64,7 @@ public function update(array $data): array return Response::error( $this->modx->lexicon('ms3_err_customer_nf'), HttpStatus::UNAUTHORIZED - )->getData(); + ); } $customerId = (int)$customer->get('id'); @@ -102,11 +103,12 @@ public function update(array $data): array if ($key === 'email') { $newEmail = trim((string) $rawValue); if (!$this->isEmailAvailable($customer, $newEmail)) { + $emailError = $this->modx->lexicon('ms3_customer_err_email_exists'); $_SESSION['ms3']['customer_profile_errors'] = [ - 'email' => $this->modx->lexicon('ms3_customer_err_email_exists'), + 'email' => $emailError, ]; - return $this->error($this->modx->lexicon('ms3_customer_err_email_exists')); + return $this->error($emailError, ['errors' => ['email' => $emailError]]); } $this->resetEmailVerificationIfChanged($customer, $newEmail); $customer->set('email', $newEmail); @@ -144,15 +146,15 @@ public function update(array $data): array * POST /api/v1/customer/add * * @param array $data Request data with key and value - * @return array ['success' => bool, 'message' => string, 'data' => array] + * @return Response ['success' => bool, 'message' => string, 'data' => array] */ - public function updateField(array $data): array + public function updateField(array $data): Response { if (empty($_SESSION['ms3']['customer_id'])) { return Response::error( $this->modx->lexicon('ms3_customer_err_login_required'), HttpStatus::UNAUTHORIZED - )->getData(); + ); } $customer = $this->getCurrentCustomer(); @@ -160,7 +162,7 @@ public function updateField(array $data): array return Response::error( $this->modx->lexicon('ms3_err_customer_nf'), HttpStatus::UNAUTHORIZED - )->getData(); + ); } $key = trim((string) ($data['key'] ?? '')); @@ -198,7 +200,9 @@ public function updateField(array $data): array } if ($key === 'email' && !$this->isEmailAvailable($customer, (string) $value)) { - return $this->error($this->modx->lexicon('ms3_customer_err_email_exists')); + $emailError = $this->modx->lexicon('ms3_customer_err_email_exists'); + + return $this->error($emailError, ['errors' => ['email' => $emailError]]); } if ($key === 'email') { @@ -308,34 +312,30 @@ protected function resetEmailVerificationIfChanged(msCustomer $customer, string } /** - * Success response - * - * @param string $message - * @param array $data - * @return array + * @param array $data */ - protected function success(string $message = '', array $data = []): array + protected function success(string $message = '', array $data = []): Response { - return [ - 'success' => true, - 'message' => $message, - 'data' => $data, - ]; + return Response::success($data, $message); } /** - * Error response + * Profile validation / business errors. + * Field map goes to top-level `errors`; also mirrored in `data.errors` for one-release BC (#572). * - * @param string $message - * @param array $data - * @return array + * @param array $data */ - protected function error(string $message, array $data = []): array + protected function error(string $message, array $data = []): Response { - return [ - 'success' => false, - 'message' => $message, - 'data' => $data, - ]; + $fieldErrors = (isset($data['errors']) && is_array($data['errors'])) ? $data['errors'] : null; + $isValidation = $fieldErrors !== null; + + return Response::errorWithCode( + $isValidation ? ApiErrorCode::VALIDATION_FAILED : ApiErrorCode::BUSINESS_RULE, + $message, + $isValidation ? HttpStatus::UNPROCESSABLE_ENTITY : HttpStatus::BAD_REQUEST, + $fieldErrors, + $data !== [] ? $data : null, + ); } } diff --git a/core/components/minishop3/src/Controllers/Api/Web/OrderController.php b/core/components/minishop3/src/Controllers/Api/Web/OrderController.php index 39d462df..8095cd3a 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/OrderController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/OrderController.php @@ -2,6 +2,8 @@ namespace MiniShop3\Controllers\Api\Web; +use MiniShop3\Router\ApiErrorCode; +use MiniShop3\Router\DomainMs2Response; use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; use MODX\Revolution\modX; @@ -29,9 +31,9 @@ public function __construct(modX $modx) * GET /api/v1/order/get * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function get(array $params = []): array + public function get(array $params = []): Response { $token = $_REQUEST['ms3_token'] ?? ''; @@ -53,9 +55,9 @@ public function get(array $params = []): array * POST /api/v1/order/add * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function add(array $params = []): array + public function add(array $params = []): Response { $input = $this->getRequestData(); @@ -71,7 +73,7 @@ public function add(array $params = []): array return Response::error( $this->modx->lexicon('ms3_err_field_key_required'), HttpStatus::BAD_REQUEST - )->getData(); + ); } $ms3 = $this->modx->services->get('ms3'); @@ -88,9 +90,9 @@ public function add(array $params = []): array * POST /api/v1/order/set * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function set(array $params = []): array + public function set(array $params = []): Response { $input = $this->getRequestData(); @@ -102,7 +104,7 @@ public function set(array $params = []): array } if (empty($fields) || !is_array($fields)) { - return Response::error($this->modx->lexicon('ms3_err_fields_required'), HttpStatus::BAD_REQUEST)->getData(); + return Response::error($this->modx->lexicon('ms3_err_fields_required'), HttpStatus::BAD_REQUEST); } $ms3 = $this->modx->services->get('ms3'); @@ -119,9 +121,9 @@ public function set(array $params = []): array * POST /api/v1/order/remove * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function remove(array $params = []): array + public function remove(array $params = []): Response { $input = $this->getRequestData(); @@ -136,7 +138,7 @@ public function remove(array $params = []): array return Response::error( $this->modx->lexicon('ms3_err_field_key_required'), HttpStatus::BAD_REQUEST - )->getData(); + ); } $ms3 = $this->modx->services->get('ms3'); @@ -146,10 +148,10 @@ public function remove(array $params = []): array $exists = $order->remove($key); if ($exists) { - return Response::success(['removed' => $key], $this->modx->lexicon('ms3_order_remove_success'))->getData(); + return Response::success(['removed' => $key], $this->modx->lexicon('ms3_order_remove_success')); } - return Response::error($this->modx->lexicon('ms3_err_field_nf'), HttpStatus::NOT_FOUND)->getData(); + return Response::error($this->modx->lexicon('ms3_err_field_nf'), HttpStatus::NOT_FOUND); } /** @@ -157,9 +159,9 @@ public function remove(array $params = []): array * POST /api/v1/order/submit * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function submit(array $params = []): array + public function submit(array $params = []): Response { $input = $this->getRequestData(); @@ -184,9 +186,9 @@ public function submit(array $params = []): array * POST /api/v1/order/clean * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function clean(array $params = []): array + public function clean(array $params = []): Response { $token = $_REQUEST['ms3_token'] ?? ''; @@ -208,9 +210,9 @@ public function clean(array $params = []): array * GET /api/v1/order/cost * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function getCost(array $params = []): array + public function getCost(array $params = []): Response { $token = $_REQUEST['ms3_token'] ?? ''; @@ -232,9 +234,9 @@ public function getCost(array $params = []): array * GET /api/v1/order/cost/cart * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function getCartCost(array $params = []): array + public function getCartCost(array $params = []): Response { $token = $_REQUEST['ms3_token'] ?? ''; @@ -256,9 +258,9 @@ public function getCartCost(array $params = []): array * GET /api/v1/order/cost/delivery * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function getDeliveryCost(array $params = []): array + public function getDeliveryCost(array $params = []): Response { $token = $_REQUEST['ms3_token'] ?? ''; @@ -280,9 +282,9 @@ public function getDeliveryCost(array $params = []): array * GET /api/v1/order/cost/payment * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function getPaymentCost(array $params = []): array + public function getPaymentCost(array $params = []): Response { $token = $_REQUEST['ms3_token'] ?? ''; @@ -304,9 +306,9 @@ public function getPaymentCost(array $params = []): array * POST /api/v1/order/address/set * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function setCustomerAddress(array $params = []): array + public function setCustomerAddress(array $params = []): Response { $input = $this->getRequestData(); @@ -319,9 +321,9 @@ public function setCustomerAddress(array $params = []): array * POST /api/v1/customer/changeAddress * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function changeCustomerAddress(array $params = []): array + public function changeCustomerAddress(array $params = []): Response { $input = $this->getRequestData(); @@ -329,7 +331,7 @@ public function changeCustomerAddress(array $params = []): array return $this->setCustomerAddressByHash($addressHash); } - protected function setCustomerAddressByHash(?string $addressHash = null): array + protected function setCustomerAddressByHash(?string $addressHash = null): Response { $token = $_REQUEST['ms3_token'] ?? ''; @@ -351,9 +353,9 @@ protected function setCustomerAddressByHash(?string $addressHash = null): array * POST /api/v1/order/address/clean * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function cleanCustomerAddress(array $params = []): array + public function cleanCustomerAddress(array $params = []): Response { $token = $_REQUEST['ms3_token'] ?? ''; @@ -375,9 +377,9 @@ public function cleanCustomerAddress(array $params = []): array * GET /api/v1/order/delivery/validation-rules * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function getDeliveryValidationRules(array $params = []): array + public function getDeliveryValidationRules(array $params = []): Response { $input = $this->getRequestData(); @@ -402,9 +404,9 @@ public function getDeliveryValidationRules(array $params = []): array * GET /api/v1/order/delivery/required-fields * * @param array $params URL parameters - * @return array Response ['success' => bool, 'message' => '', 'data' => [...]] + * @return Response */ - public function getDeliveryRequiresFields(array $params = []): array + public function getDeliveryRequiresFields(array $params = []): Response { $input = $this->getRequestData(); @@ -425,14 +427,15 @@ public function getDeliveryRequiresFields(array $params = []): array } /** - * @return array{success: bool, message: string, code: int, errors: mixed} + * @return Response */ - private function tokenRequiredError(): array + private function tokenRequiredError(): Response { - return Response::error( + return Response::errorWithCode( + ApiErrorCode::TOKEN_REQUIRED, $this->modx->lexicon('ms3_customer_err_token_required'), HttpStatus::UNAUTHORIZED - )->getData(); + ); } /** @@ -454,21 +457,19 @@ protected function getRequestData(): array } /** - * Transform Order response to API format + * Transform Order domain MS2-array to Web API Response (#572). * - * @param array $result Response from Order controller - * @return array Response in API format ['success' => bool, 'message' => '', 'data' => [...]] + * @param array $result */ - protected function transformResponse(array $result): array + protected function transformResponse(array $result): Response { - if ($result['success']) { - return Response::success($result['data'], $result['message'] ?? '')->getData(); - } else { - return Response::error( - $result['message'] ?? $this->modx->lexicon('ms3_err_unknown'), - HttpStatus::BAD_REQUEST, - $result['data'] ?? [] - )->getData(); + if (!empty($result['success'])) { + return Response::success($result['data'] ?? null, $result['message'] ?? ''); } + + return DomainMs2Response::failure( + (string) ($result['message'] ?? $this->modx->lexicon('ms3_err_unknown')), + $result['data'] ?? null, + ); } } diff --git a/core/components/minishop3/src/Middleware/RateLimitMiddleware.php b/core/components/minishop3/src/Middleware/RateLimitMiddleware.php index b3e7ef45..0eedab9e 100644 --- a/core/components/minishop3/src/Middleware/RateLimitMiddleware.php +++ b/core/components/minishop3/src/Middleware/RateLimitMiddleware.php @@ -3,6 +3,7 @@ namespace MiniShop3\Middleware; use MiniShop3\Router\Middleware\MiddlewareInterface; +use MiniShop3\Router\ApiErrorCode; use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; use MiniShop3\Services\RateLimit\FileRateLimitStore; @@ -61,7 +62,11 @@ public function handle(array $params) $retryAfter = max(0, $state['reset_at'] - time()); header("Retry-After: $retryAfter"); - return Response::error('ms3_err_rate_limit', HttpStatus::TOO_MANY_REQUESTS); + return Response::errorWithCode( + ApiErrorCode::RATE_LIMITED, + 'ms3_err_rate_limit', + HttpStatus::TOO_MANY_REQUESTS + ); } $this->setRateLimitHeaders($state['attempts'], $state['reset_at']); diff --git a/core/components/minishop3/src/Middleware/TokenMiddleware.php b/core/components/minishop3/src/Middleware/TokenMiddleware.php index 17fb82da..b20460f1 100644 --- a/core/components/minishop3/src/Middleware/TokenMiddleware.php +++ b/core/components/minishop3/src/Middleware/TokenMiddleware.php @@ -3,6 +3,7 @@ namespace MiniShop3\Middleware; use MiniShop3\Router\Middleware\MiddlewareInterface; +use MiniShop3\Router\ApiErrorCode; use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; use MiniShop3\Services\TokenService; @@ -118,7 +119,7 @@ public function handle(array $params) modX::LOG_LEVEL_INFO, '[TokenMiddleware] Rejected expired API token: ' . substr($token, 0, 16) . '...' ); - return Response::error('ms3_err_token_expired', HttpStatus::UNAUTHORIZED); + return $this->unauthorizedError(ApiErrorCode::TOKEN_EXPIRED, 'ms3_err_token_expired'); } } elseif (!$isPublic) { $this->modx->log( @@ -126,7 +127,7 @@ public function handle(array $params) '[TokenMiddleware] Token not found in database. Token: ' . substr($token, 0, 16) . '...' ); // Keep machine-stable keys in message — ApiClient.isTokenError() matches them - return Response::error('ms3_err_token_invalid', HttpStatus::UNAUTHORIZED); + return $this->unauthorizedError(ApiErrorCode::TOKEN_INVALID, 'ms3_err_token_invalid'); } } elseif (!$isPublic && !empty($_SESSION['ms3']['customer_id'])) { // Stale session identity without a resolvable token must not bypass revoke. @@ -142,12 +143,24 @@ public function handle(array $params) return null; } - return Response::error('ms3_customer_err_token_create', HttpStatus::UNAUTHORIZED); + return Response::errorWithCode( + ApiErrorCode::INTERNAL_ERROR, + 'ms3_customer_err_token_create', + HttpStatus::INTERNAL_SERVER_ERROR + ); } return null; } + /** + * Token reject / mint failure (401 + machine error_code). + */ + private function unauthorizedError(string $errorCode, string $message): Response + { + return Response::errorWithCode($errorCode, $message, HttpStatus::UNAUTHORIZED); + } + /** * Clear cookie, request and session token identity after reject/expiry. */ diff --git a/core/components/minishop3/src/Router/ApiErrorCode.php b/core/components/minishop3/src/Router/ApiErrorCode.php new file mode 100644 index 00000000..1bec07bc --- /dev/null +++ b/core/components/minishop3/src/Router/ApiErrorCode.php @@ -0,0 +1,42 @@ + self::UNAUTHORIZED, + HttpStatus::FORBIDDEN => self::FORBIDDEN, + HttpStatus::NOT_FOUND => self::NOT_FOUND, + HttpStatus::CONFLICT => self::CONFLICT, + HttpStatus::TOO_MANY_REQUESTS => self::RATE_LIMITED, + HttpStatus::UNPROCESSABLE_ENTITY => self::VALIDATION_FAILED, + HttpStatus::INTERNAL_SERVER_ERROR, + HttpStatus::SERVICE_UNAVAILABLE => self::INTERNAL_ERROR, + HttpStatus::BAD_REQUEST => self::BAD_REQUEST, + default => self::BAD_REQUEST, + }; + } +} diff --git a/core/components/minishop3/src/Router/DomainMs2Response.php b/core/components/minishop3/src/Router/DomainMs2Response.php new file mode 100644 index 00000000..b9708cea --- /dev/null +++ b/core/components/minishop3/src/Router/DomainMs2Response.php @@ -0,0 +1,120 @@ + $result + */ + public static function fromDomain(array $result, string $fallbackMessage): Response + { + if (!empty($result['success'])) { + return Response::success($result['data'] ?? null, $result['message'] ?? ''); + } + + return self::failure( + (string) ($result['message'] ?? $fallbackMessage), + $result['data'] ?? null, + ); + } + + public static function failure(string $message, mixed $data = null): Response + { + $fieldErrors = null; + $context = $data; + + if (is_array($data) && self::isFieldErrorMap($data['errors'] ?? null)) { + /** @var array $fieldErrors */ + $fieldErrors = $data['errors']; + $context = $data; + unset($context['errors']); + if ($context === []) { + $context = null; + } + } + + if ($fieldErrors !== null) { + return Response::errorWithCode( + ApiErrorCode::VALIDATION_FAILED, + $message, + HttpStatus::UNPROCESSABLE_ENTITY, + $fieldErrors, + $context, + ); + } + + if (self::looksLikeTokenError($message)) { + return Response::errorWithCode( + ApiErrorCode::TOKEN_REQUIRED, + $message, + HttpStatus::UNAUTHORIZED, + null, + $context, + ); + } + + if (self::looksLikeNotFound($message)) { + return Response::errorWithCode( + ApiErrorCode::NOT_FOUND, + $message, + HttpStatus::NOT_FOUND, + null, + $context, + ); + } + + return Response::errorWithCode( + ApiErrorCode::BUSINESS_RULE, + $message, + HttpStatus::BAD_REQUEST, + null, + $context, + ); + } + + private static function isFieldErrorMap(mixed $errors): bool + { + if (!is_array($errors) || $errors === []) { + return false; + } + + // MODX processor list: [{id, msg}, ...] — not a field map + if (array_is_list($errors)) { + $first = $errors[0] ?? null; + if (is_array($first) && (isset($first['id']) || isset($first['msg']))) { + return false; + } + } + + foreach ($errors as $value) { + if (!is_string($value) && !is_array($value) && !is_numeric($value)) { + return false; + } + } + + return true; + } + + private static function looksLikeTokenError(string $message): bool + { + $normalized = strtolower($message); + + return str_contains($normalized, 'ms3_err_token') + || str_contains($normalized, 'ms3_customer_err_token'); + } + + private static function looksLikeNotFound(string $message): bool + { + $normalized = strtolower($message); + + return str_contains($normalized, '_nf') + || str_contains($normalized, 'not found') + || str_contains($normalized, 'не найден'); + } +} diff --git a/core/components/minishop3/src/Router/Response.php b/core/components/minishop3/src/Router/Response.php index ccec35c1..1939966e 100644 --- a/core/components/minishop3/src/Router/Response.php +++ b/core/components/minishop3/src/Router/Response.php @@ -60,16 +60,48 @@ public static function success(mixed $data = null, ?string $message = null, int } /** - * Create error response + * Create error response. + * + * Envelope (#341 + #572): + * `{ success:false, message, code, errors, error_code?, data? }` + * + * - `code` — HTTP status (int), same as response status + * - `error_code` — optional stable snake_case machine key (additive) + * - `errors` — field-level map only (string|string[]|MODX {msg}); not arbitrary payload + * - `data` — optional non-field context (e.g. conflict existing_id, cart status) */ - public static function error(string $message, int $statusCode = HttpStatus::BAD_REQUEST, mixed $errors = null): self - { - return new self([ + public static function error( + string $message, + int $statusCode = HttpStatus::BAD_REQUEST, + mixed $errors = null, + ?string $errorCode = null, + mixed $data = null, + ): self { + $body = [ 'success' => false, 'message' => $message, 'code' => $statusCode, - 'errors' => $errors - ], $statusCode); + 'errors' => $errors, + 'error_code' => self::resolveErrorCode($errorCode, $statusCode, $errors), + ]; + if ($data !== null) { + $body['data'] = $data; + } + + return new self($body, $statusCode); + } + + /** + * Error with required machine `error_code` (Web API / Nuxt). + */ + public static function errorWithCode( + string $errorCode, + string $message, + int $statusCode = HttpStatus::BAD_REQUEST, + mixed $errors = null, + mixed $data = null, + ): self { + return self::error($message, $statusCode, $errors, $errorCode, $data); } /** @@ -101,26 +133,40 @@ public static function fromProcessor(object $processorResponse): self $message = self::messageFromFieldErrors($fieldErrors); } - $data = null; - if (is_array($object)) { - $data = $object; - unset($data['code']); - if ($data === []) { - $data = null; - } + return self::error( + $message, + $status, + $fieldErrors, + $fieldErrors !== null ? ApiErrorCode::VALIDATION_FAILED : null, + self::dataFromProcessorObject($object), + ); + } + + /** + * Processor failure object minus transport-only `code`. + */ + private static function dataFromProcessorObject(mixed $object): mixed + { + if (!is_array($object)) { + return null; } - $body = [ - 'success' => false, - 'message' => $message, - 'code' => $status, - 'errors' => $fieldErrors, - ]; - if ($data !== null) { - $body['data'] = $data; + $data = $object; + unset($data['code']); + + return $data !== [] ? $data : null; + } + + private static function resolveErrorCode(?string $errorCode, int $statusCode, mixed $errors = null): string + { + if ($errorCode !== null && $errorCode !== '') { + return $errorCode; + } + if (is_array($errors) && $errors !== []) { + return ApiErrorCode::VALIDATION_FAILED; } - return new self($body, $status); + return ApiErrorCode::fromHttpStatus($statusCode); } /** @@ -204,6 +250,7 @@ private static function isAllowedErrorStatus(int $code): bool return in_array($code, [ HttpStatus::BAD_REQUEST, HttpStatus::UNAUTHORIZED, + HttpStatus::FORBIDDEN, HttpStatus::NOT_FOUND, HttpStatus::CONFLICT, HttpStatus::UNPROCESSABLE_ENTITY, diff --git a/core/components/minishop3/tests/ResponseFromProcessorTest.php b/core/components/minishop3/tests/ResponseFromProcessorTest.php index cd6afca1..396b4706 100644 --- a/core/components/minishop3/tests/ResponseFromProcessorTest.php +++ b/core/components/minishop3/tests/ResponseFromProcessorTest.php @@ -10,6 +10,7 @@ require __DIR__ . '/../vendor/autoload.php'; +use MiniShop3\Router\ApiErrorCode; use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; @@ -150,6 +151,15 @@ public function getResponse(): array if (($unauth->getData()['code'] ?? null) !== HttpStatus::UNAUTHORIZED) { $fail('Response::error body must include code 401'); } +if (($unauth->getData()['error_code'] ?? null) !== ApiErrorCode::UNAUTHORIZED) { + $fail('Response::error must include additive error_code'); +} +if (($fieldError->getData()['error_code'] ?? null) !== ApiErrorCode::VALIDATION_FAILED) { + $fail('fromProcessor field errors must set error_code=validation_failed'); +} +if (($throttle->getData()['error_code'] ?? null) !== ApiErrorCode::RATE_LIMITED) { + $fail('fromProcessor 429 must set error_code=rate_limited'); +} fwrite(STDOUT, "OK ResponseFromProcessorTest\n"); exit(0); diff --git a/core/components/minishop3/tests/TokenMiddlewarePublicRoutesTest.php b/core/components/minishop3/tests/TokenMiddlewarePublicRoutesTest.php index 0430fed8..67838c43 100644 --- a/core/components/minishop3/tests/TokenMiddlewarePublicRoutesTest.php +++ b/core/components/minishop3/tests/TokenMiddlewarePublicRoutesTest.php @@ -67,13 +67,37 @@ $fail('TokenMiddleware must return raw key ms3_err_token_invalid (ApiClient contract)'); } -if (!str_contains($middlewareSrc, "Response::error('ms3_err_token_invalid'")) { - $fail('TokenMiddleware must Response::error with raw ms3_err_token_invalid'); +if ( + !str_contains($middlewareSrc, "'ms3_err_token_invalid'") + && !str_contains($middlewareSrc, '"ms3_err_token_invalid"') +) { + $fail('TokenMiddleware must pass raw ms3_err_token_invalid as message'); } -if (!str_contains($middlewareSrc, "Response::error('ms3_customer_err_token_create'")) { +if ( + !str_contains($middlewareSrc, "ApiErrorCode::TOKEN_INVALID") + && !str_contains($middlewareSrc, "Response::error('ms3_err_token_invalid'") +) { + $fail('TokenMiddleware must expose token_invalid via errorWithCode or Response::error'); +} + +if ( + !str_contains($middlewareSrc, "'ms3_customer_err_token_create'") + && !str_contains($middlewareSrc, '"ms3_customer_err_token_create"') +) { $fail('TokenMiddleware mint failure must use ms3_customer_err_token_create'); } +if (!str_contains($middlewareSrc, 'ApiErrorCode::INTERNAL_ERROR')) { + $fail('TokenMiddleware mint failure must use internal_error (not token_required)'); +} + +if ( + !str_contains($middlewareSrc, "'ms3_err_token_expired'") + && !str_contains($middlewareSrc, '"ms3_err_token_expired"') +) { + $fail('TokenMiddleware expired path must keep raw ms3_err_token_expired'); +} + fwrite(STDOUT, "OK TokenMiddlewarePublicRoutesTest\n"); exit(0); diff --git a/core/components/minishop3/tests/WebApiErrorContractTest.php b/core/components/minishop3/tests/WebApiErrorContractTest.php new file mode 100644 index 00000000..d5b099ea --- /dev/null +++ b/core/components/minishop3/tests/WebApiErrorContractTest.php @@ -0,0 +1,174 @@ + 1], 'created', HttpStatus::CREATED); +if ($created->getStatusCode() !== HttpStatus::CREATED) { + $fail('create address must preserve HTTP 201 on Response object'); +} +if (($created->getData()['success'] ?? false) !== true) { + $fail('create success envelope broken'); +} + +// Product 404 → code + error_code +$notFound = Response::error('not found', HttpStatus::NOT_FOUND); +if ($notFound->getStatusCode() !== HttpStatus::NOT_FOUND) { + $fail('product 404 status'); +} +$body = $notFound->getData(); +if (($body['code'] ?? null) !== HttpStatus::NOT_FOUND) { + $fail('product 404 body code'); +} +if (($body['error_code'] ?? null) !== ApiErrorCode::NOT_FOUND) { + $fail('product 404 error_code must be not_found'); +} + +// Profile validation → top-level errors + 422 + validation_failed +$validation = Response::errorWithCode( + ApiErrorCode::VALIDATION_FAILED, + 'validation failed', + HttpStatus::UNPROCESSABLE_ENTITY, + ['email' => ['Invalid email']], + ['errors' => ['email' => ['Invalid email']]], +); +$vBody = $validation->getData(); +if ($validation->getStatusCode() !== HttpStatus::UNPROCESSABLE_ENTITY) { + $fail('profile validation HTTP must be 422'); +} +if (($vBody['error_code'] ?? null) !== ApiErrorCode::VALIDATION_FAILED) { + $fail('profile validation error_code'); +} +if (($vBody['errors']['email'][0] ?? null) !== 'Invalid email') { + $fail('profile validation top-level errors'); +} +if (($vBody['data']['errors']['email'][0] ?? null) !== 'Invalid email') { + $fail('profile validation BC data.errors mirror'); +} + +// Cart/order domain: business_rule + payload in data (not errors) +$cartFail = Response::errorWithCode( + ApiErrorCode::BUSINESS_RULE, + 'cart rule', + HttpStatus::BAD_REQUEST, + null, + ['status' => 'empty'], +); +$cBody = $cartFail->getData(); +if (($cBody['error_code'] ?? null) !== ApiErrorCode::BUSINESS_RULE) { + $fail('cart domain error_code'); +} +if (!array_key_exists('errors', $cBody) || $cBody['errors'] !== null) { + $fail('cart domain must keep errors null'); +} +if (($cBody['data']['status'] ?? null) !== 'empty') { + $fail('cart domain payload must live in data'); +} + +// Domain MS2 transform: lift data.errors → top-level errors + 422 +$orderValidation = DomainMs2Response::failure( + 'validation failed', + ['order' => ['id' => 1], 'errors' => ['email' => 'Required']], +); +$ov = $orderValidation->getData(); +if ($orderValidation->getStatusCode() !== HttpStatus::UNPROCESSABLE_ENTITY) { + $fail('domain field map must be HTTP 422'); +} +if (($ov['error_code'] ?? null) !== ApiErrorCode::VALIDATION_FAILED) { + $fail('domain field map error_code'); +} +if (($ov['errors']['email'] ?? null) !== 'Required') { + $fail('domain field map must lift to top-level errors'); +} +if (isset($ov['data']['errors'])) { + $fail('lifted field map must not remain in data.errors'); +} +if (($ov['data']['order']['id'] ?? null) !== 1) { + $fail('domain context must remain in data'); +} + +$orderNotFound = DomainMs2Response::failure('ms3_err_order_nf', null); +if ($orderNotFound->getStatusCode() !== HttpStatus::NOT_FOUND) { + $fail('domain _nf message must map to HTTP 404'); +} +if (($orderNotFound->getData()['error_code'] ?? null) !== ApiErrorCode::NOT_FOUND) { + $fail('domain not found error_code'); +} + +// Conflict: existing_id in data +$conflict = Response::errorWithCode( + ApiErrorCode::CONFLICT, + 'exists', + HttpStatus::CONFLICT, + null, + ['existing_id' => 42], +); +if ($conflict->getStatusCode() !== HttpStatus::CONFLICT) { + $fail('address conflict status'); +} +if (($conflict->getData()['data']['existing_id'] ?? null) !== 42) { + $fail('address conflict existing_id in data'); +} +if (!array_key_exists('errors', $conflict->getData()) || $conflict->getData()['errors'] !== null) { + $fail('conflict must not stuff payload into errors'); +} + +// Token / rate-limit machine codes +$tokenExpired = Response::errorWithCode( + ApiErrorCode::TOKEN_EXPIRED, + 'ms3_err_token_expired', + HttpStatus::UNAUTHORIZED +); +if (($tokenExpired->getData()['error_code'] ?? null) !== ApiErrorCode::TOKEN_EXPIRED) { + $fail('token_expired error_code'); +} + +$rateLimited = Response::errorWithCode( + ApiErrorCode::RATE_LIMITED, + 'ms3_err_rate_limit', + HttpStatus::TOO_MANY_REQUESTS +); +if ($rateLimited->getStatusCode() !== HttpStatus::TOO_MANY_REQUESTS) { + $fail('rate limit status'); +} +if (($rateLimited->getData()['error_code'] ?? null) !== ApiErrorCode::RATE_LIMITED) { + $fail('rate_limited error_code'); +} + +// ApiErrorCode::fromHttpStatus mapping +$maps = [ + HttpStatus::UNAUTHORIZED => ApiErrorCode::UNAUTHORIZED, + HttpStatus::FORBIDDEN => ApiErrorCode::FORBIDDEN, + HttpStatus::NOT_FOUND => ApiErrorCode::NOT_FOUND, + HttpStatus::CONFLICT => ApiErrorCode::CONFLICT, + HttpStatus::TOO_MANY_REQUESTS => ApiErrorCode::RATE_LIMITED, + HttpStatus::UNPROCESSABLE_ENTITY => ApiErrorCode::VALIDATION_FAILED, + HttpStatus::BAD_REQUEST => ApiErrorCode::BAD_REQUEST, + HttpStatus::INTERNAL_SERVER_ERROR => ApiErrorCode::INTERNAL_ERROR, +]; +foreach ($maps as $status => $expected) { + if (ApiErrorCode::fromHttpStatus($status) !== $expected) { + $fail("fromHttpStatus({$status}) expected {$expected}"); + } +} + +fwrite(STDOUT, "OK WebApiErrorContractTest\n"); +exit(0); From 328d7cb02f2ba093ee4ef45ff1d8d8c552df67eb Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Sun, 16 Aug 2026 15:12:20 +0600 Subject: [PATCH 2/5] =?UTF-8?q?fix(web-api):=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20array-shape=20=D0=B8=D0=B7=20PHPDoc=20return=20R?= =?UTF-8?q?esponse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intelephense читал `@return Response ['…']` как array и ругался на Response::error. --- .../src/Controllers/Api/Web/CustomerEmailController.php | 2 +- .../src/Controllers/Api/Web/CustomerProfileController.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php b/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php index 782587cc..dda572b8 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php @@ -52,7 +52,7 @@ public function __construct(modX $modx, MiniShop3 $ms3) * * POST /api/v1/customer/email/resend-verification * - * @return Response ['success' => bool, 'message' => string] + * @return Response */ public function resendVerification(): Response { diff --git a/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php b/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php index bd428404..09ea80c5 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CustomerProfileController.php @@ -46,7 +46,7 @@ public function __construct(modX $modx, MiniShop3 $ms3) * PUT /api/v1/customer/profile * * @param array $data Form data - * @return Response ['success' => bool, 'message' => string, 'data' => array] + * @return Response */ public function update(array $data): Response { @@ -146,7 +146,7 @@ public function update(array $data): Response * POST /api/v1/customer/add * * @param array $data Request data with key and value - * @return Response ['success' => bool, 'message' => string, 'data' => array] + * @return Response */ public function updateField(array $data): Response { From 0e0d5f4e4663642a45e52a882e1ce4e8c53f16b1 Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Sun, 16 Aug 2026 15:13:26 +0600 Subject: [PATCH 3/5] =?UTF-8?q?fix(web-api):=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20success/error=20=D0=B2=20CustomerEmailController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Имя error() совпадало с Response::error и давало ложный Expected array в IDE. --- .../Api/Web/CustomerEmailController.php | 34 ++++--------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php b/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php index dda572b8..2cfaa4d2 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php @@ -154,14 +154,14 @@ public function verify(array $params): Response } // Email is verified; auto-login failed (same UX as html=1 redirect without session) - return $this->success( - $this->modx->lexicon('ms3_customer_email_verified'), + return Response::success( [ 'customer_id' => $customer->id, 'customer' => CustomerPublicDto::fromCustomer($customer, $this->modx, $this->ms3), 'token' => null, 'expires_at' => null, - ] + ], + $this->modx->lexicon('ms3_customer_email_verified') ); } @@ -174,14 +174,14 @@ public function verify(array $params): Response return Response::redirect($this->buildEmailVerificationSuccessRedirectUrl(), 302); } - return $this->success( - $this->modx->lexicon('ms3_customer_email_verified'), + return Response::success( [ 'customer_id' => $customer->id, 'customer' => CustomerPublicDto::fromCustomer($customer, $this->modx, $this->ms3), 'token' => $session['token'], 'expires_at' => $session['expires_at'], - ] + ], + $this->modx->lexicon('ms3_customer_email_verified') ); } @@ -208,26 +208,4 @@ protected function buildEmailVerificationFailedRedirectUrl(): string return $base . '?ms3_email_verified=0'; } - - /** - * @param array $data - */ - protected function success(string $message = '', array $data = []): Response - { - return Response::success($data, $message); - } - - /** - * @param array $data - */ - protected function error(string $message, array $data = []): Response - { - return Response::error( - $message, - HttpStatus::BAD_REQUEST, - null, - null, - $data !== [] ? $data : null, - ); - } } From 3926aace2ad05e93a4448ce8b91a009cefeec79e Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Sun, 16 Aug 2026 15:15:10 +0600 Subject: [PATCH 4/5] =?UTF-8?q?fix(web-api):=20=D1=8F=D0=B2=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20return=20type=20Response=20=D1=83=20=D1=84=D0=B0=D0=B1?= =?UTF-8?q?=D1=80=D0=B8=D0=BA=20=D0=B8=20EmailController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Убирает ложный Expected array у return Response::error в IDE. --- .../Api/Web/CustomerEmailController.php | 36 ++++++------------- .../minishop3/src/Router/Response.php | 10 +++--- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php b/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php index 2cfaa4d2..bb91ed70 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php @@ -1,5 +1,7 @@ modx = $modx; @@ -48,11 +41,7 @@ public function __construct(modX $modx, MiniShop3 $ms3) } /** - * Resend verification email - * * POST /api/v1/customer/email/resend-verification - * - * @return Response */ public function resendVerification(): Response { @@ -63,12 +52,12 @@ public function resendVerification(): Response ); } - $customerId = (int)$_SESSION['ms3']['customer_id']; + $customerId = (int) $_SESSION['ms3']['customer_id']; - /** @var msCustomer $customer */ + /** @var msCustomer|null $customer */ $customer = $this->modx->getObject(msCustomer::class, $customerId); - if (!$customer) { + if (!$customer instanceof msCustomer) { return Response::error( $this->modx->lexicon('ms3_err_customer_nf'), HttpStatus::UNAUTHORIZED @@ -85,7 +74,7 @@ public function resendVerification(): Response return Response::success( $result['data'] ?? null, - $result['message'] ?? '' + isset($result['message']) ? (string) $result['message'] : '' ); } @@ -96,15 +85,12 @@ public function resendVerification(): Response } /** - * Verify confirmation token from email - * * GET /api/v1/customer/email/verify?token={token} * * - `format=json` — всегда JSON (интеграции, отладка). * - `html=1` (как в ссылке из письма) — после успеха/ошибки HTTP 302 на сайт (см. GH-226). * - * @param array $params Request parameters - * @return Response + * @param array $params */ public function verify(array $params): Response { @@ -113,7 +99,7 @@ public function verify(array $params): Response $token = $params['token'] ?? ''; - if (empty($token)) { + if ($token === '') { if ($htmlFlow && !$formatJson) { return Response::redirect($this->buildEmailVerificationFailedRedirectUrl(), 302); } @@ -125,7 +111,7 @@ public function verify(array $params): Response ); } - $customer = $this->emailVerification->verifyToken($token); + $customer = $this->emailVerification->verifyToken((string) $token); if (!$customer) { if ($htmlFlow && !$formatJson) { diff --git a/core/components/minishop3/src/Router/Response.php b/core/components/minishop3/src/Router/Response.php index 1939966e..d8fabd60 100644 --- a/core/components/minishop3/src/Router/Response.php +++ b/core/components/minishop3/src/Router/Response.php @@ -34,7 +34,7 @@ public function __construct($data, int $statusCode = HttpStatus::OK, array $head /** * Redirect response (e.g. email verification in browser; api.php sends Location) */ - public static function redirect(string $url, int $statusCode = 302): self + public static function redirect(string $url, int $statusCode = 302): Response { $r = new self(null, $statusCode); $r->redirectUrl = $url; @@ -50,7 +50,7 @@ public function getRedirectUrl(): ?string /** * Create success response */ - public static function success(mixed $data = null, ?string $message = null, int $statusCode = HttpStatus::OK): self + public static function success(mixed $data = null, ?string $message = null, int $statusCode = HttpStatus::OK): Response { return new self([ 'success' => true, @@ -76,7 +76,7 @@ public static function error( mixed $errors = null, ?string $errorCode = null, mixed $data = null, - ): self { + ): Response { $body = [ 'success' => false, 'message' => $message, @@ -100,7 +100,7 @@ public static function errorWithCode( int $statusCode = HttpStatus::BAD_REQUEST, mixed $errors = null, mixed $data = null, - ): self { + ): Response { return self::error($message, $statusCode, $errors, $errorCode, $data); } @@ -119,7 +119,7 @@ public static function errorWithCode( * * @param object $processorResponse modProcessorResponse (isError/getMessage/getObject) */ - public static function fromProcessor(object $processorResponse): self + public static function fromProcessor(object $processorResponse): Response { if (!$processorResponse->isError()) { return self::success($processorResponse->getObject(), $processorResponse->getMessage()); From d8f182992fede2ef3fb970d1c3c030e20ef73067 Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Sun, 16 Aug 2026 15:16:15 +0600 Subject: [PATCH 5/5] =?UTF-8?q?fix(web-api):=20Order/Cart=20domain=20map?= =?UTF-8?q?=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20DomainMs2Response::fromDoma?= =?UTF-8?q?in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Убирает transformResponse в OrderController, из‑за которого IDE видела return array. --- .../Controllers/Api/Web/CartController.php | 13 ++- .../Controllers/Api/Web/OrderController.php | 81 ++++++++++++------- .../src/Router/DomainMs2Response.php | 3 + 3 files changed, 60 insertions(+), 37 deletions(-) diff --git a/core/components/minishop3/src/Controllers/Api/Web/CartController.php b/core/components/minishop3/src/Controllers/Api/Web/CartController.php index 74c1b185..ebffe0f4 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CartController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CartController.php @@ -257,6 +257,7 @@ protected function getRequestData(): array * Transform Cart domain MS2-array to Web API Response (#572). * * @param array $result + * @return Response */ protected function transformResponse(array $result): Response { @@ -267,18 +268,14 @@ protected function transformResponse(array $result): Response $customerToken = $_REQUEST['ms3_token'] ?? ''; $renderedHtml = $this->renderSnippets($renderTokens, $customerToken); - if (!empty($renderedHtml)) { + if (!empty($renderedHtml) && is_array($result['data'] ?? null)) { $result['data']['render'] = $renderedHtml; } } - if (!empty($result['success'])) { - return Response::success($result['data'] ?? null, $result['message'] ?? ''); - } - - return DomainMs2Response::failure( - (string) ($result['message'] ?? $this->modx->lexicon('ms3_err_unknown')), - $result['data'] ?? null, + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') ); } diff --git a/core/components/minishop3/src/Controllers/Api/Web/OrderController.php b/core/components/minishop3/src/Controllers/Api/Web/OrderController.php index 8095cd3a..4049859c 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/OrderController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/OrderController.php @@ -47,7 +47,10 @@ public function get(array $params = []): Response $result = $order->get(); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -82,7 +85,10 @@ public function add(array $params = []): Response $result = $order->add($key, $value); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -113,7 +119,10 @@ public function set(array $params = []): Response $result = $order->set($fields); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -178,7 +187,10 @@ public function submit(array $params = []): Response $result = $order->submit($data); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -202,7 +214,10 @@ public function clean(array $params = []): Response $result = $order->clean(); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -226,7 +241,10 @@ public function getCost(array $params = []): Response $result = $order->getCost(false); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -250,7 +268,10 @@ public function getCartCost(array $params = []): Response $result = $order->getCartCost(); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -274,7 +295,10 @@ public function getDeliveryCost(array $params = []): Response $result = $order->getDeliveryCost(); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -298,7 +322,10 @@ public function getPaymentCost(array $params = []): Response $result = $order->getPaymentCost(); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -345,7 +372,10 @@ protected function setCustomerAddressByHash(?string $addressHash = null): Respon $result = $order->setCustomerAddress($addressHash); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -369,7 +399,10 @@ public function cleanCustomerAddress(array $params = []): Response $result = $order->cleanCustomerAddress(); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -396,7 +429,10 @@ public function getDeliveryValidationRules(array $params = []): Response $result = $order->getDeliveryValidationRules($delivery_id); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -423,7 +459,10 @@ public function getDeliveryRequiresFields(array $params = []): Response $result = $order->getDeliveryRequiresFields($delivery_id); - return $this->transformResponse($result); + return DomainMs2Response::fromDomain( + $result, + $this->modx->lexicon('ms3_err_unknown') + ); } /** @@ -456,20 +495,4 @@ protected function getRequestData(): array return array_merge($_GET, $_POST); } - /** - * Transform Order domain MS2-array to Web API Response (#572). - * - * @param array $result - */ - protected function transformResponse(array $result): Response - { - if (!empty($result['success'])) { - return Response::success($result['data'] ?? null, $result['message'] ?? ''); - } - - return DomainMs2Response::failure( - (string) ($result['message'] ?? $this->modx->lexicon('ms3_err_unknown')), - $result['data'] ?? null, - ); - } } diff --git a/core/components/minishop3/src/Router/DomainMs2Response.php b/core/components/minishop3/src/Router/DomainMs2Response.php index b9708cea..05dd2bd2 100644 --- a/core/components/minishop3/src/Router/DomainMs2Response.php +++ b/core/components/minishop3/src/Router/DomainMs2Response.php @@ -24,6 +24,9 @@ public static function fromDomain(array $result, string $fallbackMessage): Respo ); } + /** + * @return Response + */ public static function failure(string $message, mixed $data = null): Response { $fieldErrors = null;