From 9820f97216044536c39cc7a2431a360524663f14 Mon Sep 17 00:00:00 2001 From: Anton Pomorzin Date: Fri, 26 Jun 2026 12:34:55 +0500 Subject: [PATCH] =?UTF-8?q?H-2626:=20admin=20shop=20API=20v2=20=D0=B2=20sw?= =?UTF-8?q?agger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 28 эндпоинтов под /admin/shop/* с 4 новыми тегами (admin-shop-product, admin-shop-product-option, admin-shop-order, admin-shop-delivery-address). Эндпоинты: - product: index/items/view + create/update/delete + export-excel + категории через legacy CategoryAction'ы (view-/update-/delete-category) - product-option: index (с обязательным shop_product_id) / view / CRUD - order: index/view + process/cancel + позиции (remove/restore/change-comment) + export-excel - delivery-address: CRUD Схемы AdminShopProduct/ProductOption/Order/DeliveryAddress (+List + SaveRequest) и HttpJsonResult для обёрток process/cancel. --- v2/swagger.yaml | 925 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 925 insertions(+) diff --git a/v2/swagger.yaml b/v2/swagger.yaml index f7cca68..9a03507 100644 --- a/v2/swagger.yaml +++ b/v2/swagger.yaml @@ -79,6 +79,23 @@ tags: description: Methods for admin oversight of personal API tokens. Requires "api-tokens-admin" permission. - name: admin-employment description: Methods for administrative read-only access to employee employments, including external identifiers used by integrations. Requires "user-edit" permission. + - name: admin-shop-product + description: | + Управление товарами корпоративного магазина (admin). Требуется право `shop-items` + и флаг тенанта `enableShop`. Action `items` доступен и менеджеру заказов (`shop-orders`). + - name: admin-shop-product-option + description: | + Варианты (опции) товаров магазина: точечный CRUD по одной опции. + Пакетное сохранение опций — через `POST /admin/shop/product/update`. + Требуется право `shop-items`. + - name: admin-shop-order + description: | + Обработка заказов магазина: подтверждение, отмена, действия с позициями. + Требуется право `shop-orders`. + - name: admin-shop-delivery-address + description: | + Справочник адресов доставки для самовывоза/курьерской доставки. Требуется + право `shop-orders`. paths: /mobile/bind/{id}/{token}: @@ -8264,6 +8281,675 @@ paths: Запись не найдена. Включает случаи обращения к записям, принадлежащим другим тенантам. + # ============================================================ + # Admin Shop API (H-2626) + # ============================================================ + + /admin/shop/product: + get: + tags: [admin-shop-product] + summary: Список товаров магазина (admin) + operationId: adminShopProductIndex + parameters: + - name: page + in: query + schema: { type: integer, minimum: 1, default: 1 } + - name: per-page + in: query + schema: { type: integer, minimum: 1, maximum: 100, default: 20 } + - name: sort + in: query + schema: { type: string } + description: Префикс `-` для DESC. Поддерживается `ShopProductSearch::sort()`. + - name: title + in: query + schema: { type: string } + description: Поиск по подстроке в названии. + - name: status_id + in: query + schema: { type: integer } + description: По умолчанию `Status::ACTIVE=1`. + - name: category_id + in: query + schema: { type: string, format: uuid } + responses: + 200: + description: Список товаров + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProductList' + 403: + description: Нет права `shop-items` или флаг `enableShop` выключен. + + /admin/shop/product/items: + get: + tags: [admin-shop-product] + summary: Лёгкий справочник активных товаров + description: | + Возвращает только `{id, title}` для использования в фильтрах админ-страниц + (например, фильтр по товару на странице обработки заказов). Доступно + дополнительно роли `shop-orders`. + operationId: adminShopProductItems + parameters: + - name: title + in: query + schema: { type: string } + responses: + 200: + description: Справочник + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + properties: + id: { type: string, format: uuid } + title: { type: string } + _meta: + $ref: '#/components/schemas/_meta' + + /admin/shop/product/view: + get: + tags: [admin-shop-product] + summary: Карточка товара (admin) + operationId: adminShopProductView + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + responses: + 200: + description: Товар + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProduct' + 400: + description: Параметр `id` имеет некорректный формат. + 404: + description: Товар не найден. + + /admin/shop/product/create: + post: + tags: [admin-shop-product] + summary: Создать товар вместе с опциями + description: | + Сохраняет товар и массив его опций одной транзакцией. Опции, отсутствующие + в `ShopProductOption[]`, помечаются `status_id = DELETED`. + + Сначала валидируется товар; затем сохраняется с `refresh()` (чтобы + вернуть defaults из БД); затем сохраняются опции (валидируется на + save — `shop_product_id` имеет правило `exist`, которое отрабатывает + только после insert товара). + operationId: adminShopProductCreate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProductSaveRequest' + responses: + 200: + description: Сохранённый товар + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProduct' + 422: + description: Ошибка валидации — пустой `ShopProductOption[]` или валидация модели не прошла. + + /admin/shop/product/update: + post: + tags: [admin-shop-product] + summary: Обновить товар вместе с опциями + operationId: adminShopProductUpdate + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProductSaveRequest' + responses: + 200: + description: Обновлённый товар + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProduct' + 404: { description: Товар не найден. } + 422: { description: Ошибка валидации. } + + /admin/shop/product/delete: + delete: + tags: [admin-shop-product] + summary: Soft-delete товара + description: | + Через override `BaseModel::deleteAll → softDeleteAll`. Физического удаления + записи не происходит — переводится в `status_id = DELETED`. + operationId: adminShopProductDelete + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + responses: + 204: { description: Удалено. } + 400: { description: Параметр `id` имеет некорректный формат. } + 404: { description: Товар не найден. } + + /admin/shop/product/export-excel: + post: + tags: [admin-shop-product] + summary: Поставить задачу на excel-экспорт списка товаров + description: Файл готовится асинхронно — статус смотреть через `/file-processing/view`. + operationId: adminShopProductExportExcel + requestBody: + required: false + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: true + responses: + 200: { description: Задача поставлена. } + + /admin/shop/product/view-category: + get: + tags: [admin-shop-product] + summary: Получить категорию товаров магазина + description: | + Реюз legacy `app\custom\actions\category\ViewAction`, параметризованного + `CategoryRoot::ROOT_SHOP_PRODUCT`. + operationId: adminShopProductViewCategory + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + responses: + 200: + description: Категория + content: + application/json: + schema: + $ref: '#/components/schemas/HttpJsonResult' + + /admin/shop/product/update-category: + post: + tags: [admin-shop-product] + summary: Создать/обновить категорию товаров магазина + operationId: adminShopProductUpdateCategory + parameters: + - name: id + in: query + required: false + schema: { type: string, format: uuid } + description: Без `id` — создание; с `id` — обновление. + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: true + description: Поля категории (title, parent_id, image и т.д. — см. CatalogManager). + responses: + 200: + description: Категория + content: + application/json: + schema: + $ref: '#/components/schemas/HttpJsonResult' + 201: + description: Создана. + + /admin/shop/product/delete-category: + delete: + tags: [admin-shop-product] + summary: Удалить категорию товаров магазина + operationId: adminShopProductDeleteCategory + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + responses: + 204: { description: Удалено. } + 400: { description: Параметр `id` имеет некорректный формат. } + + /admin/shop/product-option: + get: + tags: [admin-shop-product-option] + summary: Варианты товара (admin) + description: | + `shop_product_id` обязателен — его требует `ShopProductOptionSearch`. + Дефолтная сортировка `sort_index ASC` через `defaultOrder` search-модели. + operationId: adminShopProductOptionIndex + parameters: + - name: shop_product_id + in: query + required: true + schema: { type: string, format: uuid } + - name: status_id + in: query + schema: { type: integer } + - name: title + in: query + schema: { type: string } + - name: sort + in: query + schema: + type: string + enum: [sort_index, "-sort_index", name, "-name", created_at, "-created_at"] + - name: per-page + in: query + schema: { type: integer, minimum: 1, maximum: 100, default: 100 } + responses: + 200: + description: Список опций + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProductOptionList' + + /admin/shop/product-option/view: + get: + tags: [admin-shop-product-option] + summary: Одна опция + operationId: adminShopProductOptionView + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + responses: + 200: + description: Опция + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProductOption' + 404: { description: Опция не найдена. } + + /admin/shop/product-option/create: + post: + tags: [admin-shop-product-option] + summary: Создать одну опцию + description: | + Для пакетного сохранения опций вместе с товаром — `POST /admin/shop/product/update`. + Этот эндпоинт нужен для точечных операций. + operationId: adminShopProductOptionCreate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProductOptionSaveRequest' + responses: + 200: + description: Созданная опция + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProductOption' + + /admin/shop/product-option/update: + post: + tags: [admin-shop-product-option] + summary: Обновить одну опцию + operationId: adminShopProductOptionUpdate + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProductOptionSaveRequest' + responses: + 200: + description: Обновлённая опция + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopProductOption' + 404: { description: Опция не найдена. } + + /admin/shop/product-option/delete: + delete: + tags: [admin-shop-product-option] + summary: Soft-delete опции + operationId: adminShopProductOptionDelete + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + responses: + 204: { description: Удалено. } + 404: { description: Опция не найдена. } + + /admin/shop/order: + get: + tags: [admin-shop-order] + summary: Список заказов (admin) + operationId: adminShopOrderIndex + parameters: + - name: page + in: query + schema: { type: integer, minimum: 1, default: 1 } + - name: per-page + in: query + schema: { type: integer, minimum: 1, maximum: 100, default: 20 } + - name: order_status_id + in: query + schema: { type: integer } + description: | + Статусы заказа из `ShopOrderStatus`: 1=NEW, 2=PROCESSED, + 3=CANCELED_USER, 4=CANCELED_MANAGER. + - name: created_user_id + in: query + schema: { type: string, format: uuid } + - name: approver_user_id + in: query + schema: { type: string, format: uuid } + - name: shop_item_id + in: query + schema: { type: string, format: uuid } + description: Фильтр по товару (через JOIN на shopCart). + - name: created_at + in: query + schema: { type: string } + description: Диапазон `<начало>|<конец>` в формате `yyyy-MM-dd`. + - name: sort + in: query + schema: { type: string } + responses: + 200: + description: Список заказов + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopOrderList' + + /admin/shop/order/view: + get: + tags: [admin-shop-order] + summary: Детализация заказа + operationId: adminShopOrderView + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + responses: + 200: + description: Заказ с расширенным составом (shopCart) + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopOrder' + + /admin/shop/order/process: + post: + tags: [admin-shop-order] + summary: Подтвердить заказ менеджером + description: | + Переводит заказ из STATUS_NEW в STATUS_PROCESSED. Если есть позиции + с `status_id = DELETED` — возвращает их количество на склад и проводит + refund-транзакцию на сумму отменённых позиций. + + Внутри транзакции: `lock()` + `refresh()` + повторная проверка статуса + NEW — защита от race condition двойного refund. + operationId: adminShopOrderProcess + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopOrderActionRequest' + responses: + 200: + description: Заказ обработан + content: + application/json: + schema: + $ref: '#/components/schemas/HttpJsonResult' + 422: + description: | + Ошибка валидации формы либо заказ уже не в статусе NEW + (`ShopOrderException::ALREADY_PROCESSED`). + + /admin/shop/order/cancel: + post: + tags: [admin-shop-order] + summary: Отменить заказ менеджером + description: | + Переводит заказ в STATUS_CANCELED_MANAGER, возвращает все позиции + на склад и проводит полный refund баланса покупателя. Уведомляет + покупателя об отмене. + operationId: adminShopOrderCancel + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopOrderActionRequest' + responses: + 200: + description: Заказ отменён + content: + application/json: + schema: + $ref: '#/components/schemas/HttpJsonResult' + 422: + description: Заказ уже не в статусе NEW или другая ошибка валидации. + + /admin/shop/order/position-remove: + post: + tags: [admin-shop-order] + summary: Удалить позицию заказа + operationId: adminShopOrderPositionRemove + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [id, comment] + properties: + id: { type: string, format: uuid, description: ID позиции (ShopCart). } + comment: { type: string, description: Причина удаления. } + responses: + 200: + description: Результат + content: + application/json: + schema: + $ref: '#/components/schemas/HttpJsonResult' + + /admin/shop/order/position-restore: + post: + tags: [admin-shop-order] + summary: Восстановить удалённую позицию заказа + operationId: adminShopOrderPositionRestore + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [id] + properties: + id: { type: string, format: uuid } + responses: + 200: + description: Результат + content: + application/json: + schema: + $ref: '#/components/schemas/HttpJsonResult' + + /admin/shop/order/position-change-comment: + post: + tags: [admin-shop-order] + summary: Изменить комментарий к позиции заказа + operationId: adminShopOrderPositionChangeComment + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [id, comment] + properties: + id: { type: string, format: uuid } + comment: { type: string } + responses: + 200: + description: Результат + content: + application/json: + schema: + $ref: '#/components/schemas/HttpJsonResult' + + /admin/shop/order/export-excel: + post: + tags: [admin-shop-order] + summary: Поставить задачу на excel-экспорт списка заказов + description: Файл готовится асинхронно — статус смотреть через `/file-processing/view`. + operationId: adminShopOrderExportExcel + requestBody: + required: false + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: true + responses: + 200: { description: Задача поставлена. } + + /admin/shop/delivery-address: + get: + tags: [admin-shop-delivery-address] + summary: Список адресов доставки + operationId: adminShopDeliveryAddressIndex + parameters: + - name: page + in: query + schema: { type: integer, minimum: 1, default: 1 } + - name: per-page + in: query + schema: { type: integer, minimum: 1, maximum: 100, default: 20 } + - name: title + in: query + schema: { type: string } + responses: + 200: + description: Список + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopDeliveryAddressList' + + /admin/shop/delivery-address/view: + get: + tags: [admin-shop-delivery-address] + summary: Один адрес + operationId: adminShopDeliveryAddressView + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + responses: + 200: + description: Адрес + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopDeliveryAddress' + + /admin/shop/delivery-address/create: + post: + tags: [admin-shop-delivery-address] + summary: Создать адрес + operationId: adminShopDeliveryAddressCreate + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [title] + properties: + title: { type: string, maxLength: 255 } + responses: + 200: + description: Адрес + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopDeliveryAddress' + + /admin/shop/delivery-address/update: + post: + tags: [admin-shop-delivery-address] + summary: Обновить адрес + operationId: adminShopDeliveryAddressUpdate + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [title] + properties: + title: { type: string, maxLength: 255 } + responses: + 200: + description: Адрес + content: + application/json: + schema: + $ref: '#/components/schemas/AdminShopDeliveryAddress' + + /admin/shop/delivery-address/delete: + delete: + tags: [admin-shop-delivery-address] + summary: Удалить адрес + description: | + Hard-delete (у модели нет soft-delete-цепочки). На исторические заказы + удаление не влияет — `ShopOrder.delivery_address` хранит текстовую копию. + operationId: adminShopDeliveryAddressDelete + parameters: + - name: id + in: query + required: true + schema: { type: string, format: uuid } + responses: + 204: { description: Удалено. } + 404: { description: Не найден. } + components: securitySchemes: SessionAuth: @@ -14013,6 +14699,245 @@ components: $ref: '#/components/schemas/GoalItem' description: Цели которые не были обработаны + # ============================================================ + # Admin Shop API (H-2626) + # ============================================================ + + HttpJsonResult: + type: object + description: | + Стандартная обёртка ответа: `app\logic\core\Entities\HttpJsonResult`. + properties: + success: { type: boolean } + data: + oneOf: + - { type: object, additionalProperties: true } + - { type: 'null' } + errors: + type: array + items: + type: object + properties: + field: { type: string } + message: { type: string } + message: + type: object + nullable: true + properties: + type: + type: string + enum: [success, info, warning, error] + text: { type: string } + + AdminShopProduct: + type: object + properties: + id: { type: string, format: uuid } + created_at: { type: string } + updated_at: { type: string } + status_id: + type: integer + description: 1=Active, 2=Inactive, 3=Deleted + preview_file_id: { type: string, format: uuid, nullable: true } + title: { type: string } + description: { type: string, nullable: true } + price: { type: integer } + createdAt: { type: string, description: Локализованная дата для UI. } + statusName: { type: string } + totalCount: + type: integer + description: Сумма `count` по всем активным опциям. + preview: { type: string, description: URL ресайз-превью. } + images: + type: array + items: { type: object, additionalProperties: true } + options: + type: array + items: + $ref: '#/components/schemas/AdminShopProductOption' + entityId: { type: integer, description: 405 (ShopProduct в Entity.php). } + # forView=true: + content: { type: object, additionalProperties: true, description: JSON-блоки EditorJS. } + categoriesName: { type: object, additionalProperties: true } + categoriesText: { type: string, nullable: true } + + AdminShopProductList: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/AdminShopProduct' + _links: + $ref: '#/components/schemas/_links' + _meta: + $ref: '#/components/schemas/_meta' + + AdminShopProductSaveRequest: + type: object + required: [title, price, options] + properties: + title: { type: string } + description: { type: string } + price: { type: integer, minimum: 0, maximum: 999999 } + preview_file_id: { type: string, format: uuid, nullable: true } + status_id: { type: integer } + options: + type: array + minItems: 1 + description: | + Полный массив опций товара. Опции, отсутствующие в массиве, помечаются + `status_id = DELETED`. Для существующих опций передаётся `id`. + items: + type: object + required: [name, count, price] + properties: + id: { type: string, format: uuid, description: Для обновления существующей опции. } + name: { type: string, maxLength: 128 } + count: { type: integer, minimum: 0, maximum: 999999 } + price: { type: integer, minimum: 0, maximum: 999999 } + preview_file_id: { type: string, format: uuid, nullable: true } + images: + type: array + items: { type: object, additionalProperties: true } + images: + oneOf: + - { type: array, items: { type: object, additionalProperties: true } } + - { type: string, description: JSON-encoded массив (legacy). } + content: + oneOf: + - { type: object, additionalProperties: true } + - { type: string, description: JSON-encoded блоки (legacy). } + + AdminShopProductOption: + type: object + properties: + id: { type: string, format: uuid } + created_at: { type: string } + updated_at: { type: string } + shop_product_id: { type: string, format: uuid } + status_id: { type: integer } + sort_index: { type: integer } + name: { type: string } + count: { type: integer } + price: { type: integer } + preview_file_id: { type: string, format: uuid, nullable: true } + images: + type: array + items: { type: object, additionalProperties: true } + entityId: { type: integer } + + AdminShopProductOptionList: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/AdminShopProductOption' + _links: + $ref: '#/components/schemas/_links' + _meta: + $ref: '#/components/schemas/_meta' + + AdminShopProductOptionSaveRequest: + type: object + description: | + Поля `id`, `created_user_id`, `updated_user_id`, `tenant_id` подставляются + автоматически в `BaseModel::beforeSave` — попытка передать их в payload + игнорируется (поля убраны из `safeAttributes`). + properties: + shop_product_id: { type: string, format: uuid } + status_id: { type: integer } + sort_index: { type: integer } + name: { type: string, maxLength: 128 } + count: { type: integer, minimum: 0, maximum: 999999 } + price: { type: integer, minimum: 0, maximum: 999999 } + preview_file_id: { type: string, format: uuid, nullable: true } + images: + type: array + items: { type: object, additionalProperties: true } + + AdminShopOrder: + type: object + properties: + id: { type: string, format: uuid } + created_at: { type: string } + updated_at: { type: string } + created_user_id: { type: string, format: uuid, description: Покупатель. } + approver_user_id: { type: string, format: uuid, nullable: true, description: Менеджер-аппрувер. } + num: { type: integer, description: Номер заказа. } + order_status_id: + type: integer + enum: [1, 2, 3, 4] + description: 1=NEW, 2=PROCESSED, 3=CANCELED_USER, 4=CANCELED_MANAGER + delivery_address: { type: string, nullable: true } + count_cart_for_sorting: { type: integer } + sum_cart_for_sorting: { type: integer } + createdAt: { type: string, description: Локализованная дата. } + orderStatusName: { type: string } + countCartPositions: { type: integer } + countCartTotal: { type: integer } + sumCart: { type: integer } + user: + type: object + additionalProperties: true + description: Профиль покупателя. + entityId: { type: integer, description: 406 (ShopOrder в Entity.php). } + # forView=true expand: + shopCart: + type: array + description: Состав заказа. + items: + type: object + additionalProperties: true + orderContent: + type: string + description: Форматированный текст состава заказа. + + AdminShopOrderList: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/AdminShopOrder' + _links: + $ref: '#/components/schemas/_links' + _meta: + $ref: '#/components/schemas/_meta' + + AdminShopOrderActionRequest: + type: object + required: [id] + properties: + id: { type: string, format: uuid, description: ID заказа. } + message: + type: string + maxLength: 512 + description: Комментарий к действию (сохраняется как Comment к заказу). + + AdminShopDeliveryAddress: + type: object + properties: + id: { type: string, format: uuid } + created_at: { type: string } + updated_at: { type: string } + created_user_id: { type: string, format: uuid } + updated_user_id: { type: string, format: uuid, nullable: true } + title: { type: string, maxLength: 255 } + + AdminShopDeliveryAddressList: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/AdminShopDeliveryAddress' + _links: + $ref: '#/components/schemas/_links' + _meta: + $ref: '#/components/schemas/_meta' + security: - SessionAuth: [ ] - ApiKeyAuth: [ ]