1219 lines
46 KiB
YAML
1219 lines
46 KiB
YAML
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 <jwt>`
|
||
(алгоритм `RS512`). Если дополнительно передан заголовок
|
||
`Identity: Bearer <jwt>`, полезная нагрузка берётся из него
|
||
(`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 <jwt>` (RS512). Опционально —
|
||
`Identity: Bearer <jwt>` для режима 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" }
|