openapi: 3.0.3 info: title: RFI project API version: "0.0.1" description: | REST API сервиса **rfi-backend** (`proc/rfi-backend`) — управление запросами RFI (Request For Information), сообщениями к ним, статусами и приоритетами (и их моделями), а также журналом изменений. Сервис написан на Python (**Django 5.1 / Django REST Framework**). Роутинг строится `DefaultRouter` (`src/config/api/v1/router.py`), в который собираются роутеры приложений `request`, `message`, `status`, `priority`, `change_history`. Все ресурсы доступны под префиксом `/api/v1/` (`config/urls.py` → `config/api/urls.py` → `config/api/v1/urls.py`). Схема OpenAPI генерируется `drf-spectacular` и доступна по `/api/schema/`, Swagger UI — по `/api/schema/swagger-ui/`, ReDoc — по `/api/schema/redoc/`. Django-admin — по `/admin/`. ### Аутентификация Все эндпоинты требуют аутентификации (`DEFAULT_PERMISSION_CLASSES = IsAuthenticated`). Режим задаётся переменной `JWT_AUTH_ENABLE` (`config/settings/base.py`): 1. **JWT включён** (`JWT_AUTH_ENABLE=true`) — активны `ZitadelJWTStatelessUserAuthentication` и `JWTStatelessUserAuthentication`. Основной токен передаётся заголовком `Authorization: Bearer ` (алгоритм `RS512`). Если дополнительно передан заголовок `Identity: Bearer `, полезная нагрузка берётся из него (`urn:zitadel:iam:user:metadata`); подпись при этом сервисом не проверяется (доверие обеспечивается сетевым слоем/Istio). 2. **JWT выключен** (`JWT_AUTH_ENABLE=false`) — `DefaultUserAuthentication` подставляет демо-пользователя `DefaultUser` (только для локальной разработки). ### Авторизация (права) Эндпоинты `rfi` и `messages` используют `RFITokenBasedPermission`: проверяется принадлежность к компании (`company_id ∈ user.company_ids`) и модульные права из токена (`core.can_view_RFI`, `core.can_create_RFI`, `core.can_edit_RFI`, `core.can_delete_RFI`, `core.can_copy_RFI`). Редактирование доступно автору или ответственному (service account), либо администратору. ### Пагинация Списочные ответы используют `LimitOffsetPagination` (`PAGE_SIZE = 100`). Ответ оборачивается в объект `{ count, next, previous, results }`. Параметры — `limit` и `offset`. ### Фильтрация Списки RFI поддерживают фильтрацию как через query-параметры (GET), так и через тело запроса (POST `/api/v1/rfi/filter/`) — за счёт `GetAndPostDjangoFilterBackend`. Дополнительно `CompanyFilterBackend` неявно ограничивает выборку компаниями пользователя, если `company_id` не передан явно. ### Особенности - Удаление объектов — «мягкое» (soft-delete через `deleted_at`), базовый класс `core.models.BaseModel`. `DELETE` возвращает `204`. - Создание/обновление RFI принимает `multipart/form-data` (есть поле-файл `image`); ответ формируется сериализатором чтения (`RFIRetrieveSerializer`). - При создании RFI и сообщений отправляются уведомления (Celery-задачи), если включено (`USE_ASYNC_FUNCTIONS`, `NOTIFICATIONS_ENABLE`). contact: name: rfi-backend url: https://gitlab/proc/rfi-backend servers: - url: https://api.sarex.io/rfi description: Production (ingress, префикс /rfi) - url: https://stage-api.sarex.io/rfi description: Stage (ingress, префикс /rfi) - url: http://rfi-backend.rfi-prod.svc.cluster.local description: Внутрикластерный адрес (ClusterIP, порт 80 → 8000) - url: http://localhost:8000 description: Локальный запуск (uwsgi / runserver, порт 8000) tags: - name: rfi description: Запросы RFI — CRUD, копирование, фильтрация, счётчики, история - name: messages description: Сообщения в запросах RFI - name: statuses description: Статусы - name: status-models description: Модели статусов - name: priorities description: Приоритеты - name: priority-models description: Модели приоритетов - name: change-history description: Журнал изменений RFI (только чтение) security: - bearerAuth: [] paths: # ========================================================================== # RFI # ========================================================================== /api/v1/rfi/: get: tags: [rfi] summary: Список RFI description: Постраничный список RFI с фильтрацией через query-параметры. parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Offset" - { name: id, in: query, schema: { type: string }, description: "Один или несколько id (повторяющийся параметр)" } - { name: resource_id, in: query, schema: { type: string }, description: "UUID ресурса (повторяющийся)" } - { name: author_id, in: query, schema: { type: string } } - { name: company_id, in: query, schema: { type: string } } - { name: status_id, in: query, schema: { type: string } } - { name: priority_id, in: query, schema: { type: string } } - { name: responsible_users, in: query, schema: { type: string }, description: "UUID service account (overlap)" } - { name: created_at_gte, in: query, schema: { type: string, format: date-time } } - { name: created_at_lte, in: query, schema: { type: string, format: date-time } } - { name: message_at_gte, in: query, schema: { type: string, format: date-time }, description: "Есть сообщение с created_at >= значения" } - { name: message_at_lte, in: query, schema: { type: string, format: date-time } } responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/PaginatedRFIList" "403": { $ref: "#/components/responses/Forbidden" } post: tags: [rfi] summary: Создать RFI requestBody: required: true content: multipart/form-data: schema: $ref: "#/components/schemas/RFIWrite" application/json: schema: $ref: "#/components/schemas/RFIWrite" responses: "201": description: Создано content: application/json: schema: { $ref: "#/components/schemas/RFI" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/rfi/{id}/: parameters: - $ref: "#/components/parameters/PathId" get: tags: [rfi] summary: Получить RFI по id responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/RFI" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [rfi] summary: Полное обновление RFI requestBody: required: true content: multipart/form-data: schema: { $ref: "#/components/schemas/RFIWrite" } application/json: schema: { $ref: "#/components/schemas/RFIWrite" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/RFI" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } patch: tags: [rfi] summary: Частичное обновление RFI requestBody: required: true content: multipart/form-data: schema: { $ref: "#/components/schemas/RFIWrite" } application/json: schema: { $ref: "#/components/schemas/RFIWrite" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/RFI" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } delete: tags: [rfi] summary: Удалить RFI (soft-delete) responses: "204": { description: Удалено } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /api/v1/rfi/filter/: post: tags: [rfi] summary: Список RFI по фильтру (POST) description: | Аналог `GET /api/v1/rfi/`, но фильтры передаются в теле запроса. Тело валидируется `RFIFilterRequestSerializer`. Пагинация — через query-параметры `limit`/`offset`. parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Offset" requestBody: required: false content: application/json: schema: { $ref: "#/components/schemas/RFIFilterRequest" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PaginatedRFIList" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/rfi/history/: get: tags: [rfi] summary: История изменений по списку RFI description: | Журнал изменений для всех RFI, попадающих под те же фильтры, что и список (`GET /api/v1/rfi/`). Отсортирован по `created_at` убыв. parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Offset" - { name: resource_id, in: query, schema: { type: string } } - { name: company_id, in: query, schema: { type: string } } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PaginatedRFIChangeRecordList" } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/rfi/{id}/history/: parameters: - $ref: "#/components/parameters/PathId" get: tags: [rfi] summary: История изменений одного RFI parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Offset" responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PaginatedRFIChangeRecordList" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /api/v1/rfi/status-count/: get: tags: [rfi] summary: Количество RFI по статусам description: Для каждого `resource_id` возвращает его статусы и количество RFI. parameters: - { name: resource_id, in: query, schema: { type: string } } - { name: company_id, in: query, schema: { type: string } } responses: "200": description: OK content: application/json: schema: type: array items: { $ref: "#/components/schemas/RFIStatusCount" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [rfi] summary: Количество RFI по статусам (POST-фильтр) requestBody: required: false content: application/json: schema: { $ref: "#/components/schemas/RFIFilterRequest" } responses: "200": description: OK content: application/json: schema: type: array items: { $ref: "#/components/schemas/RFIStatusCount" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/rfi/priority-count/: get: tags: [rfi] summary: Количество RFI по приоритетам parameters: - { name: resource_id, in: query, schema: { type: string } } - { name: company_id, in: query, schema: { type: string } } responses: "200": description: OK content: application/json: schema: type: array items: { $ref: "#/components/schemas/RFIPriorityCount" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [rfi] summary: Количество RFI по приоритетам (POST-фильтр) requestBody: required: false content: application/json: schema: { $ref: "#/components/schemas/RFIFilterRequest" } responses: "200": description: OK content: application/json: schema: type: array items: { $ref: "#/components/schemas/RFIPriorityCount" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/rfi/{id}/copy/: parameters: - $ref: "#/components/parameters/PathId" post: tags: [rfi] summary: Копировать RFI description: Создаёт копию RFI с новым именем. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CopyName" } responses: "201": description: Создано content: application/json: schema: { $ref: "#/components/schemas/RFI" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } # ========================================================================== # Messages # ========================================================================== /api/v1/messages/: get: tags: [messages] summary: Список сообщений parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Offset" - { name: id, in: query, schema: { type: string } } - { name: author_id, in: query, schema: { type: string } } - { name: request_id, in: query, schema: { type: string } } - { name: reply_to_id, in: query, schema: { type: string } } - { name: company_id, in: query, schema: { type: string } } - { name: resource_id, in: query, schema: { type: string, format: uuid } } - { name: created_at_gte, in: query, schema: { type: string, format: date-time } } - { name: created_at_lte, in: query, schema: { type: string, format: date-time } } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PaginatedMessageList" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [messages] summary: Создать сообщение requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/MessageWrite" } responses: "201": description: Создано content: application/json: schema: { $ref: "#/components/schemas/MessageWrite" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/messages/{id}/: parameters: - $ref: "#/components/parameters/PathId" get: tags: [messages] summary: Сообщение по id responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Message" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [messages] summary: Обновить сообщение requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/MessageWrite" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/MessageWrite" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } patch: tags: [messages] summary: Частичное обновление сообщения (напр. отметить решением) requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/MessageWrite" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/MessageWrite" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } delete: tags: [messages] summary: Удалить сообщение (soft-delete) responses: "204": { description: Удалено } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } # ========================================================================== # Statuses # ========================================================================== /api/v1/statuses/: get: tags: [statuses] summary: Список статусов parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Offset" - { name: company_id, in: query, schema: { type: string } } - { name: status_model, in: query, schema: { type: integer }, description: "id модели статусов (повторяющийся)" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PaginatedStatusList" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [statuses] summary: Создать статус requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/StatusCreate" } responses: "201": description: Создано content: application/json: schema: { $ref: "#/components/schemas/StatusCreate" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/statuses/{id}/: parameters: - $ref: "#/components/parameters/PathId" get: tags: [statuses] summary: Статус по id responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Status" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [statuses] summary: Обновить статус requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/StatusCreate" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/StatusCreate" } "400": { $ref: "#/components/responses/ValidationError" } "404": { $ref: "#/components/responses/NotFound" } patch: tags: [statuses] summary: Частичное обновление статуса requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/StatusCreate" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/StatusCreate" } "404": { $ref: "#/components/responses/NotFound" } delete: tags: [statuses] summary: Удалить статус (soft-delete) responses: "204": { description: Удалено } "404": { $ref: "#/components/responses/NotFound" } # ========================================================================== # Status models # ========================================================================== /api/v1/status-models/: get: tags: [status-models] summary: Список моделей статусов parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Offset" - { name: company_id, in: query, schema: { type: integer } } - { name: resource_id, in: query, schema: { type: string, format: uuid } } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PaginatedStatusModelList" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [status-models] summary: Создать модель статусов description: | При создании первой модели статусов для компании автоматически создаётся набор статусов по умолчанию (`DEFAULT_STATUSES`). requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/StatusModelWrite" } responses: "201": description: Создано content: application/json: schema: { $ref: "#/components/schemas/StatusModelWrite" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/status-models/{id}/: parameters: - $ref: "#/components/parameters/PathId" get: tags: [status-models] summary: Модель статусов по id responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/StatusModelDetail" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [status-models] summary: Обновить модель статусов requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/StatusModelWrite" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/StatusModelWrite" } "404": { $ref: "#/components/responses/NotFound" } patch: tags: [status-models] summary: Частичное обновление модели статусов requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/StatusModelWrite" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/StatusModelWrite" } "404": { $ref: "#/components/responses/NotFound" } delete: tags: [status-models] summary: Удалить модель статусов (soft-delete) responses: "204": { description: Удалено } "404": { $ref: "#/components/responses/NotFound" } # ========================================================================== # Priorities # ========================================================================== /api/v1/priorities/: get: tags: [priorities] summary: Список приоритетов parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Offset" - { name: company_id, in: query, schema: { type: string } } - { name: priority_model, in: query, schema: { type: integer }, description: "id модели приоритетов (повторяющийся)" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PaginatedPriorityList" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [priorities] summary: Создать приоритет requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/PriorityWrite" } responses: "201": description: Создано content: application/json: schema: { $ref: "#/components/schemas/PriorityWrite" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/priorities/{id}/: parameters: - $ref: "#/components/parameters/PathId" get: tags: [priorities] summary: Приоритет по id responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Priority" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [priorities] summary: Обновить приоритет requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/PriorityWrite" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PriorityWrite" } "404": { $ref: "#/components/responses/NotFound" } patch: tags: [priorities] summary: Частичное обновление приоритета requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/PriorityWrite" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PriorityWrite" } "404": { $ref: "#/components/responses/NotFound" } delete: tags: [priorities] summary: Удалить приоритет (soft-delete) responses: "204": { description: Удалено } "404": { $ref: "#/components/responses/NotFound" } # ========================================================================== # Priority models # ========================================================================== /api/v1/priority-models/: get: tags: [priority-models] summary: Список моделей приоритетов parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Offset" - { name: company_id, in: query, schema: { type: integer } } - { name: resource_id, in: query, schema: { type: string, format: uuid } } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PaginatedPriorityModelList" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [priority-models] summary: Создать модель приоритетов description: | При создании первой модели приоритетов для компании автоматически создаётся набор приоритетов по умолчанию (`DEFAULT_PRIORITIES`). requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/PriorityModelWrite" } responses: "201": description: Создано content: application/json: schema: { $ref: "#/components/schemas/PriorityModelWrite" } "400": { $ref: "#/components/responses/ValidationError" } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/priority-models/{id}/: parameters: - $ref: "#/components/parameters/PathId" get: tags: [priority-models] summary: Модель приоритетов по id responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PriorityModel" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [priority-models] summary: Обновить модель приоритетов requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/PriorityModelWrite" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PriorityModelWrite" } "404": { $ref: "#/components/responses/NotFound" } patch: tags: [priority-models] summary: Частичное обновление модели приоритетов requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/PriorityModelWrite" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PriorityModelWrite" } "404": { $ref: "#/components/responses/NotFound" } delete: tags: [priority-models] summary: Удалить модель приоритетов (soft-delete) responses: "204": { description: Удалено } "404": { $ref: "#/components/responses/NotFound" } # ========================================================================== # Change history (read-only) # ========================================================================== /api/v1/change-history/: get: tags: [change-history] summary: Журнал изменений RFI parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Offset" - { name: request_id, in: query, schema: { type: integer } } - { name: resource_id, in: query, schema: { type: string, format: uuid } } - { name: company_id, in: query, schema: { type: string } } - { name: created_at_gte, in: query, schema: { type: string, format: date-time } } - { name: created_at_lte, in: query, schema: { type: string, format: date-time } } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PaginatedRFIChangeRecordList" } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/change-history/{id}/: parameters: - $ref: "#/components/parameters/PathId" get: tags: [change-history] summary: Запись журнала изменений по id responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/RFIChangeRecord" } "404": { $ref: "#/components/responses/NotFound" } components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: | `Authorization: Bearer ` (RS512). Опционально — `Identity: Bearer ` для режима Zitadel. parameters: Limit: name: limit in: query required: false schema: { type: integer, default: 100 } description: Количество элементов на странице (LimitOffsetPagination). Offset: name: offset in: query required: false schema: { type: integer } description: Смещение от начала выборки. PathId: name: id in: path required: true schema: { type: integer } responses: ValidationError: description: Ошибка валидации (DRF) content: application/json: schema: type: object additionalProperties: true example: { field_name: ["Обязательное поле."] } Forbidden: description: Недостаточно прав / не аутентифицирован content: application/json: schema: { $ref: "#/components/schemas/Detail" } NotFound: description: Не найдено content: application/json: schema: { $ref: "#/components/schemas/Detail" } schemas: Detail: type: object properties: detail: { type: string } Link: type: object properties: name: { type: string, maxLength: 255 } link: { type: string, format: uri } required: [name, link] RFI: description: Чтение RFI (RFIListSerializer / RFIRetrieveSerializer). type: object properties: id: { type: integer, readOnly: true } name: { type: string, maxLength: 255 } author_id: { type: integer } description: { type: string, nullable: true } company_id: { type: integer } resource_id: { type: string, format: uuid, nullable: true } responsible_users: type: array items: { type: string, format: uuid } completion_date: { type: string, format: date-time, nullable: true } image: { type: string, nullable: true, description: "URL изображения (по умолчанию rfi-default.png)" } attributes: { type: object, additionalProperties: true } links: type: array items: { $ref: "#/components/schemas/Link" } created_at: { type: string, format: date-time, readOnly: true } updated_at: { type: string, format: date-time, readOnly: true } status: { type: integer, description: "id статуса (status_id)" } priority: { type: integer, description: "id приоритета (priority_id)" } RFIWrite: description: Запись RFI (RFIWriteSerializer). type: object properties: name: { type: string, maxLength: 255 } author_id: { type: integer } description: { type: string, nullable: true } company_id: { type: integer } resource_id: { type: string, format: uuid, nullable: true } responsible_users: type: array items: { type: string, format: uuid } completion_date: { type: string, format: date-time, nullable: true } image: { type: string, format: binary, nullable: true } attributes: { type: object, additionalProperties: true, default: {} } links: type: array items: { $ref: "#/components/schemas/Link" } status_id: { type: integer, description: "PK статуса" } priority_id: { type: integer, description: "PK приоритета" } required: [name, author_id, company_id, responsible_users, status_id, priority_id] RFIFilterRequest: description: Тело фильтра для POST /rfi/filter/, /rfi/status-count/, /rfi/priority-count/. type: object properties: id: { type: array, items: { type: string } } resource_id: { type: array, items: { type: string } } author_id: { type: array, items: { type: string } } company_id: { type: integer } status_id: { type: array, items: { type: string } } priority_id: { type: array, items: { type: string } } responsible_users: { type: array, items: { type: string } } created_at_gte: { type: string, format: date-time } created_at_lte: { type: string, format: date-time } message_at_gte: { type: string, format: date-time } message_at_lte: { type: string, format: date-time } CopyName: type: object properties: name: { type: string, maxLength: 255 } required: [name] Message: description: Чтение сообщения (MessageReadSerializer). type: object properties: id: { type: integer, readOnly: true } author_id: { type: integer } text: { type: string } is_solution: { type: boolean } request: { type: integer, description: "id запроса (request_id)" } reply_to: { type: integer, nullable: true, description: "id родительского сообщения" } created_at: { type: string, format: date-time, readOnly: true } updated_at: { type: string, format: date-time, readOnly: true } MessageWrite: description: Запись сообщения (MessageWriteSerializer). type: object properties: author_id: { type: integer } text: { type: string } is_solution: { type: boolean } request_id: { type: integer } reply_to_id: { type: integer, nullable: true } required: [author_id, text, request_id] Status: description: Чтение статуса (StatusReadSerializer). type: object properties: id: { type: integer, readOnly: true } name: { type: string, maxLength: 512 } label: { type: string, maxLength: 512 } color: { type: string, description: "HEX-цвет", example: "#FFFFFF" } status_model: { type: integer, description: "id модели статусов" } StatusShort: type: object properties: id: { type: integer } name: { type: string } label: { type: string } color: { type: string } StatusCreate: description: Создание/обновление статуса (StatusCreateSerializer). type: object properties: name: { type: string, maxLength: 512 } label: { type: string, maxLength: 512 } color: { type: string, example: "#FFFFFF" } status_model: { type: integer, description: "PK модели статусов" } required: [name, label, status_model] StatusModelList: type: object properties: id: { type: integer, readOnly: true } name: { type: string, maxLength: 512 } company_id: { type: integer } resource_id: { type: string, format: uuid, nullable: true } statuses: type: array items: { $ref: "#/components/schemas/StatusShort" } StatusModelDetail: allOf: - $ref: "#/components/schemas/StatusModelList" - type: object properties: created_at: { type: string, format: date-time, readOnly: true } updated_at: { type: string, format: date-time, readOnly: true } StatusModelWrite: type: object properties: name: { type: string, maxLength: 512 } company_id: { type: integer } resource_id: { type: string, format: uuid, nullable: true } required: [name, company_id] Priority: description: Чтение приоритета (PriorityReadSerializer). type: object properties: id: { type: integer, readOnly: true } name: { type: string, maxLength: 512 } label: { type: string, maxLength: 512 } color: { type: string, example: "#FFFFFF" } priority_model: { type: integer, description: "id модели приоритетов" } PriorityShort: type: object properties: id: { type: integer } name: { type: string } label: { type: string } color: { type: string } PriorityWrite: description: Создание/обновление приоритета (PriorityWriteSerializer). type: object properties: name: { type: string, maxLength: 512 } label: { type: string, maxLength: 512 } color: { type: string, example: "#FFFFFF" } priority_model_id: { type: integer, description: "PK модели приоритетов (write-only)" } required: [name, label, priority_model_id] PriorityModel: description: Чтение модели приоритетов (PriorityModelReadSerializer). type: object properties: id: { type: integer, readOnly: true } name: { type: string, maxLength: 512 } company_id: { type: integer } resource_id: { type: string, format: uuid, nullable: true } priorities: type: array items: { $ref: "#/components/schemas/PriorityShort" } PriorityModelWrite: type: object properties: name: { type: string, maxLength: 512 } company_id: { type: integer } resource_id: { type: string, format: uuid, nullable: true } required: [name, company_id] CountItem: type: object properties: id: { type: integer } name: { type: string } label: { type: string } color: { type: string } count: { type: integer } RFIStatusCount: type: object properties: resource_id: { type: string, format: uuid } statuses: type: array items: { $ref: "#/components/schemas/CountItem" } RFIPriorityCount: type: object properties: resource_id: { type: string, format: uuid } priorities: type: array items: { $ref: "#/components/schemas/CountItem" } RFIChangeRecord: description: Запись журнала изменений (RFIChangeRecordSerializer). type: object properties: id: { type: integer, readOnly: true } created_at: { type: string, format: date-time, readOnly: true } request_id: { type: integer, readOnly: true } created_by: { type: integer, description: "ID пользователя, совершившего изменение" } field_name: { type: string, maxLength: 128 } attribute_name: { type: string, maxLength: 256, nullable: true } was: { type: string, description: "Старое значение (raw)" } became: { type: string, description: "Новое значение (raw)" } was_text: { type: string, description: "Человекочитаемое старое значение" } became_text: { type: string, description: "Человекочитаемое новое значение" } # ---- Пагинированные обёртки (LimitOffsetPagination) ---- PaginatedBase: type: object properties: count: { type: integer } next: { type: string, format: uri, nullable: true } previous: { type: string, format: uri, nullable: true } PaginatedRFIList: allOf: - $ref: "#/components/schemas/PaginatedBase" - type: object properties: results: type: array items: { $ref: "#/components/schemas/RFI" } PaginatedMessageList: allOf: - $ref: "#/components/schemas/PaginatedBase" - type: object properties: results: type: array items: { $ref: "#/components/schemas/Message" } PaginatedStatusList: allOf: - $ref: "#/components/schemas/PaginatedBase" - type: object properties: results: type: array items: { $ref: "#/components/schemas/Status" } PaginatedStatusModelList: allOf: - $ref: "#/components/schemas/PaginatedBase" - type: object properties: results: type: array items: { $ref: "#/components/schemas/StatusModelList" } PaginatedPriorityList: allOf: - $ref: "#/components/schemas/PaginatedBase" - type: object properties: results: type: array items: { $ref: "#/components/schemas/Priority" } PaginatedPriorityModelList: allOf: - $ref: "#/components/schemas/PaginatedBase" - type: object properties: results: type: array items: { $ref: "#/components/schemas/PriorityModel" } PaginatedRFIChangeRecordList: allOf: - $ref: "#/components/schemas/PaginatedBase" - type: object properties: results: type: array items: { $ref: "#/components/schemas/RFIChangeRecord" }