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" }
