openapi: 3.0.3

info:
  title: Transmittal Service API
  version: "1.0.0"
  description: |
    REST API сервиса **transmittal-api** (`pdm/transmittal-api`) — управление
    трансмитталами (передачей документации на согласование), их шаблонами,
    шагами/действиями маршрута согласования и генерацией актов.

    Сервис написан на Python (**FastAPI**). Приложение собирается фабрикой
    `get_application` в `src/transmittal_service/app/http_server.py`. Роутинг
    состоит из двух групп:

    - публичный API — префикс `/api/v1` (`controller/http/api/v1/api.py`);
    - внутренний API — префикс `/internal/v1` (`controller/http/internal/v1/api.py`),
      предназначен для вызовов внутри кластера (через ingress не публикуется).

    Интерактивная документация Swagger доступна по `/api/docs`, схема —
    по `/api/openapi.json` (с учётом `root_path`).

    ### Аутентификация
    Публичные эндпоинты (кроме `/api/v1/healthcheck`) требуют аутентификации.
    Токен передаётся заголовком `Authorization: Bearer <jwt>` (FastAPI
    `HTTPBearer`). Поддерживаются два режима (`controller/http/security.py`):

    1. **Zitadel** — если передан дополнительный заголовок `identity`
       (`Identity <jwt>`), полезная нагрузка берётся из этого токена
       (`urn:zitadel:iam:user:metadata`). Валидность проверяется на уровне
       Istio, подпись сервисом не проверяется.
    2. **sarex-backend** — если заголовка `identity` нет, подпись основного
       токена проверяется публичным RSA-ключом
       (`TRANSMITTAL_SERVICE_AUTH__PUBLIC_KEY`, алгоритм `RS512`).

    Внутренние эндпоинты (`/internal/v1/*`) аутентификации на уровне приложения
    не требуют — ограничение доступа обеспечивается сетевым слоем.

    ### Пагинация
    Списочные ответы используют «bookmark»-пагинацию. Ответ оборачивается в
    `Page` — `{ result, next, prev }`, где `next`/`prev` — курсоры (bookmark).
    Размер страницы задаётся параметром `limit` (в теле или query, зависит от
    эндпоинта), продолжение — параметром `bookmark`. Часть ответов оборачивается
    в более простой `Result` — `{ result }` (без курсоров).

    ### Обработка ошибок
    Ошибки возвращаются в формате **RFC 7807** (`application/problem+json`),
    схема `ApiError` — `{ type, title, status, detail, instance }`
    (`controller/http/middleware.py`, класс `ExceptionHandler`). Идентификатор
    запроса возвращается в заголовке `X-Request-Id`, длительность обработки —
    в `Server-Timing`.

    ### Замечания (расхождения кода)
    - Ошибка «ресурс не найден» (`NotFound`) маппится на статус **`410 Gone`**,
      а не на привычный `404 Not Found`.
    - При отсутствии/некорректности `Authorization` FastAPI `HTTPBearer`
      возвращает `403`, тогда как ошибки разбора токена в middleware дают `401`.
    - Ошибки валидации тела/параметров запроса (Pydantic) отдаются FastAPI в
      стандартном формате `422` (не `application/problem+json`).
    - Конфликт имени шаблона (`TemplateNameIsNotUnique`) возвращает `409`.

  contact:
    name: transmittal-api
    url: https://gitlab/pdm/transmittal-api

servers:
  - url: https://api.sarex.io/transmittals
    description: Production (ingress, root_path=/transmittals)
  - url: https://stage-api.sarex.io/transmittals
    description: Stage (ingress, root_path=/transmittals)
  - url: http://transmittal-service.transmittal-api-stage.svc.cluster.local
    description: Внутрикластерный адрес (ClusterIP, порт 80 → 8000). Единственный способ достучаться до /internal/v1
  - url: http://localhost:8001
    description: Локальный запуск (Uvicorn, порт по умолчанию 8001)

tags:
  - name: infra
    description: Служебные эндпоинты (проверка доступности)
  - name: transmittals
    description: Трансмитталы — создание, просмотр, согласование, поиск, акты
  - name: transmittal_templates
    description: Шаблоны трансмитталов
  - name: steps
    description: Шаги маршрута согласования
  - name: internal
    description: Внутренние эндпоинты (только внутри кластера)

security:
  - bearerAuth: []

paths:
  # ==========================================================================
  # Infra
  # ==========================================================================
  /api/v1/healthcheck:
    get:
      tags: [infra]
      summary: Проверка доступности зависимостей
      description: |
        Параллельно проверяет доступность всех внешних зависимостей (БД, S3,
        и HTTP-сервисов) и возвращает агрегированный статус `healthy` или
        `partially_healthy`. Аутентификация не требуется.
      operationId: healthcheck
      security: []
      responses:
        '200':
          description: Статус доступности зависимостей
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthCheckResponse'

  # ==========================================================================
  # Steps
  # ==========================================================================
  /api/v1/steps:
    get:
      tags: [steps]
      summary: Список шагов
      operationId: getSteps
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 32767
            default: 100
        - name: bookmark
          in: query
          required: false
          schema:
            type: string
            nullable: true
      responses:
        '200':
          description: Страница шагов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Page_StepReadResponseDto'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/steps/{id}:
    get:
      tags: [steps]
      summary: Шаг по id
      operationId: getStep
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 0
            maximum: 32767
      responses:
        '200':
          description: Шаг
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StepReadResponseDto'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationError' }

  # ==========================================================================
  # Transmittals
  # ==========================================================================
  /api/v1/transmittals:
    post:
      tags: [transmittals]
      summary: Список трансмитталов ресурса
      description: Пагинированный список трансмитталов по `resource_id` со сводной статистикой по статусам.
      operationId: listTransmittals
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransmittalListRequestDto'
      responses:
        '200':
          description: Страница трансмитталов и статистика по статусам
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransmittalListResponseDto'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittals/create:
    post:
      tags: [transmittals]
      summary: Создать трансмиттал
      description: |
        Создаёт трансмиттал. Требуется хотя бы один документ (`documents_to_approve`)
        и хотя бы один получатель (`receivers`).
      operationId: createTransmittal
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransmittalCreateRequestDto'
      responses:
        '200':
          description: Идентификатор созданного трансмиттала
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransmittalCreateResponseDto'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittals/global_statuses:
    post:
      tags: [transmittals]
      summary: Глобальные статусы по набору ресурсов
      description: Возвращает статистику статусов трансмитталов по списку `resource_ids`.
      operationId: getGlobalStatuses
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransmittalGetGlobalStatusesRequestDto'
      responses:
        '200':
          description: Статистика статусов по ресурсам
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransmittalGetGlobalStatusesResponseDto'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittals/status:
    get:
      tags: [transmittals]
      summary: Справочник статусов
      operationId: listStatuses
      responses:
        '200':
          description: Список типов статусов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusesListResponseDto'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/transmittals/count:
    get:
      tags: [transmittals]
      summary: Число открытых трансмитталов пользователя
      description: Количество незакрытых трансмитталов, требующих действия текущего пользователя.
      operationId: myOpenTransmittalsCount
      responses:
        '200':
          description: Количество открытых трансмитталов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransmittalOpenCountResponseDto'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/transmittals/search:
    post:
      tags: [transmittals]
      summary: Поиск трансмитталов в ресурсе
      description: Поиск и фильтрация трансмитталов в рамках одного ресурса (`resource_id`).
      operationId: searchTransmittals
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransmittalSearchFilterRequestDto'
      responses:
        '200':
          description: Страница трансмитталов и статистика по статусам
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransmittalListResponseDto'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittals/search/resources:
    post:
      tags: [transmittals]
      summary: Поиск по нескольким ресурсам
      description: Фильтрация трансмитталов по списку ресурсов (`resource_ids`); возвращает статистику статусов по ресурсам.
      operationId: searchTransmittalsResources
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransmittalSearchFilterDto'
      responses:
        '200':
          description: Статистика статусов по ресурсам
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransmittalGetGlobalStatusesResponseDto'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittals/{transmittal_id}:
    parameters:
      - $ref: '#/components/parameters/TransmittalId'
    get:
      tags: [transmittals]
      summary: Трансмиттал по id
      operationId: getTransmittal
      responses:
        '200':
          description: Трансмиттал с историей действий и документами
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransmittalReadResponseDto'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      tags: [transmittals]
      summary: Удалить трансмиттал
      operationId: deleteTransmittal
      responses:
        '204': { description: Удалено }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittals/{transmittal_id}/approve:
    parameters:
      - $ref: '#/components/parameters/TransmittalId'
    put:
      tags: [transmittals]
      summary: Принять трансмиттал
      description: Фиксирует действие «принять» текущего пользователя на текущем шаге (с опциональным комментарием). По завершении может инициировать генерацию акта.
      operationId: approveTransmittal
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserActionCreateRequest'
      responses:
        '200':
          description: Созданное действие пользователя
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserActionCreateResponseDto'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittals/{transmittal_id}/decline:
    parameters:
      - $ref: '#/components/parameters/TransmittalId'
    put:
      tags: [transmittals]
      summary: Отклонить трансмиттал
      description: Фиксирует действие «отклонить» текущего пользователя на текущем шаге (с опциональным комментарием).
      operationId: declineTransmittal
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserActionCreateRequest'
      responses:
        '200':
          description: Созданное действие пользователя
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserActionCreateResponseDto'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittals/{transmittal_id}/act_available:
    parameters:
      - $ref: '#/components/parameters/TransmittalId'
    get:
      tags: [transmittals]
      summary: Доступность акта
      description: Признак готовности акта для скачивания по данному трансмитталу.
      operationId: actAvailable
      responses:
        '200':
          description: Готовность акта
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransmittalGetActReadinessResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittals/{transmittal_id}/download_act:
    parameters:
      - $ref: '#/components/parameters/TransmittalId'
    get:
      tags: [transmittals]
      summary: Скачать акт
      description: Возвращает presigned-URL для скачивания акта из S3.
      operationId: downloadAct
      responses:
        '200':
          description: Presigned-URL акта
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransmittalDownloadActResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittals/{transmittal_id}/link_review:
    parameters:
      - $ref: '#/components/parameters/TransmittalId'
    post:
      tags: [transmittals]
      summary: Привязать review к трансмитталу
      operationId: linkReview
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransmittalLinkReviewRequest'
      responses:
        '204': { description: Review привязан }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationError' }

  # ==========================================================================
  # Transmittal templates
  # ==========================================================================
  /api/v1/transmittal_templates:
    post:
      tags: [transmittal_templates]
      summary: Список шаблонов трансмитталов
      description: Пагинированный список шаблонов с фильтрами. Тело запроса опционально (при отсутствии применяются значения по умолчанию).
      operationId: listTransmittalTemplates
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransmittalTemplateListFiltersRequest'
      responses:
        '200':
          description: Страница шаблонов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Page_TransmittalTemplateListResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittal_templates/create:
    post:
      tags: [transmittal_templates]
      summary: Создать шаблон
      operationId: createTransmittalTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransmittalTemplateCreateRequestDto'
      responses:
        '200':
          description: Идентификатор созданного шаблона
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransmittalTemplateCreateResponseDto'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittal_templates/select:
    get:
      tags: [transmittal_templates]
      summary: Шаблоны для выбора
      description: Плоский список шаблонов (id/имя/автор), доступных для выбора в рамках ресурса.
      operationId: listTransmittalTemplatesForSelect
      parameters:
        - name: resource
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: template_id
          in: query
          required: false
          description: Один или несколько id шаблонов для фильтрации
          schema:
            type: array
            items:
              type: string
              format: uuid
            nullable: true
      responses:
        '200':
          description: Список шаблонов для выбора
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Result_TransmittalTemplateSelectListResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/transmittal_templates/{transmittal_template_id}:
    parameters:
      - $ref: '#/components/parameters/TransmittalTemplateId'
    get:
      tags: [transmittal_templates]
      summary: Шаблон по id
      operationId: getTransmittalTemplate
      parameters:
        - name: resource
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Шаблон трансмиттала
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransmittalTemplateGetSingleResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationError' }
    patch:
      tags: [transmittal_templates]
      summary: Обновить шаблон
      operationId: updateTransmittalTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransmittalTemplateUpdateRequest'
      responses:
        '204': { description: Обновлено }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      tags: [transmittal_templates]
      summary: Удалить шаблон
      operationId: deleteTransmittalTemplate
      responses:
        '204': { description: Удалено }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationError' }

  # ==========================================================================
  # Internal (cluster-only)
  # ==========================================================================
  /internal/v1/transmittals:
    post:
      tags: [internal]
      summary: Список трансмитталов (внутренний)
      description: Внутренний список трансмитталов по фильтрам. Без аутентификации на уровне приложения; доступен только внутри кластера.
      operationId: internalGetTransmittals
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransmittalListInternalRequest'
      responses:
        '200':
          description: Список трансмитталов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Result_TransmittalReadListResponseDto'
        '422': { $ref: '#/components/responses/ValidationError' }

  /internal/v1/transmittals/by_bundle_ids:
    post:
      tags: [internal]
      summary: Трансмитталы по bundle id (внутренний)
      description: Возвращает трансмитталы, связанные с переданными `bundle_ids`. Без аутентификации на уровне приложения; доступен только внутри кластера.
      operationId: internalGetTransmittalsByBundleIds
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransmittalGetByBundleIdsRequest'
      responses:
        '200':
          description: Трансмитталы по bundle id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Result_TransmittalGetByBundleIdsResponse'
        '422': { $ref: '#/components/responses/ValidationError' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT в заголовке `Authorization: Bearer <token>`. Проверяется публичным
        ключом (`RS512`) в режиме sarex-backend либо принимается как есть в
        режиме Zitadel (см. заголовок `identity`).
    identityToken:
      type: apiKey
      in: header
      name: identity
      description: |
        Опциональный заголовок `identity` (`Identity <jwt>`) для режима Zitadel.
        При его наличии полезная нагрузка берётся из этого токена, а подпись
        основного токена сервисом не проверяется.

  parameters:
    TransmittalId:
      name: transmittal_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    TransmittalTemplateId:
      name: transmittal_template_id
      in: path
      required: true
      schema:
        type: string
        format: uuid

  responses:
    BadRequest:
      description: Некорректный запрос / бизнес-правило нарушено
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ApiError'
    Unauthorized:
      description: Токен не предоставлен или невалиден
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ApiError'
    Forbidden:
      description: Недостаточно прав
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ApiError'
    Conflict:
      description: Конфликт (например, имя шаблона уже занято)
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ApiError'
    Gone:
      description: Запрошенный ресурс не существует (маппинг `NotFound` → `410`)
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ApiError'
    ValidationError:
      description: Ошибка валидации тела/параметров запроса (FastAPI/Pydantic)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/HTTPValidationError'

  schemas:
    # --- Общие ------------------------------------------------------------
    ApiError:
      type: object
      description: Ошибка в формате RFC 7807 (application/problem+json)
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        instance:
          type: string
          description: URL запроса, вызвавшего ошибку
      required: [type, title, status, detail, instance]

    HTTPValidationError:
      type: object
      properties:
        detail:
          type: array
          items:
            $ref: '#/components/schemas/ValidationErrorItem'

    ValidationErrorItem:
      type: object
      properties:
        loc:
          type: array
          items:
            anyOf:
              - type: string
              - type: integer
        msg: { type: string }
        type: { type: string }
      required: [loc, msg, type]

    ReceiversRequestDto:
      type: object
      description: Получатели трансмиттала (запрос). Хотя бы один из наборов должен быть непуст.
      properties:
        users:
          type: array
          items: { type: integer, minimum: 0, maximum: 9223372036854775807 }
          uniqueItems: true
        departments:
          type: array
          items: { type: integer }
          uniqueItems: true
        roles:
          type: array
          items: { type: integer }
          uniqueItems: true

    ReceiversReadResponseDto:
      type: object
      properties:
        users: { type: array, items: { type: integer } }
        departments: { type: array, items: { type: integer } }
        roles: { type: array, items: { type: integer } }
      required: [users, departments, roles]

    ReceiversFilterDto:
      type: object
      properties:
        users: { type: array, items: { type: integer } }
        departments: { type: array, items: { type: integer } }
        roles: { type: array, items: { type: integer } }

    DocumentVersionRequestDto:
      type: object
      properties:
        document_id: { type: integer, minimum: 0, maximum: 2147483647 }
        bundle_id: { type: string, format: uuid }
      required: [document_id, bundle_id]

    DocumentVersionReadResponse:
      type: object
      properties:
        document_id: { type: integer }
        bundle_id: { type: string, format: uuid }
      required: [document_id, bundle_id]

    StatusReadResponseDto:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        slug: { type: string }
      required: [id, name, slug]

    StatusSimpleDto:
      type: object
      properties:
        slug: { type: string }
        name: { type: string }
      required: [slug, name]

    StatusesListResponseDto:
      type: object
      properties:
        statuses:
          type: array
          items: { $ref: '#/components/schemas/StatusSimpleDto' }
      required: [statuses]

    StepReadResponseDto:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        slug: { type: string }
      required: [id, name, slug]

    ActionReadResponse:
      type: object
      properties:
        id: { type: integer }
        step_id: { type: integer }
        name: { type: string }
        slug: { type: string }
      required: [id, step_id, name, slug]

    ReviewLightResponse:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
      required: [id, name]

    UserActionCreateRequest:
      type: object
      properties:
        comment: { type: string, nullable: true }

    UserActionCreateResponseDto:
      type: object
      properties:
        id: { type: string, format: uuid }
        transmittal_id: { type: string, format: uuid }
        action_id: { type: integer }
        comment: { type: string, nullable: true }
      required: [id, transmittal_id, action_id, comment]

    UserActionReadResponse:
      type: object
      properties:
        id: { type: string, format: uuid }
        user_id: { type: integer }
        transmittal_id: { type: string, format: uuid }
        action_id: { type: integer }
        comment: { type: string, nullable: true }
        action: { $ref: '#/components/schemas/ActionReadResponse' }
        created_at: { type: string, format: date-time }
      required: [id, user_id, transmittal_id, action_id, comment, action, created_at]

    # --- Wrappers ---------------------------------------------------------
    Page_StepReadResponseDto:
      type: object
      properties:
        result:
          type: array
          items: { $ref: '#/components/schemas/StepReadResponseDto' }
        next: { type: string }
        prev: { type: string }
      required: [result, next, prev]

    Page_TransmittalReadListResponseDto:
      type: object
      properties:
        result:
          type: array
          items: { $ref: '#/components/schemas/TransmittalReadListResponseDto' }
        next: { type: string }
        prev: { type: string }
      required: [result, next, prev]

    Page_TransmittalTemplateListResponse:
      type: object
      properties:
        result:
          type: array
          items: { $ref: '#/components/schemas/TransmittalTemplateListResponse' }
        next: { type: string }
        prev: { type: string }
      required: [result, next, prev]

    Result_TransmittalTemplateSelectListResponse:
      type: object
      properties:
        result:
          type: array
          items: { $ref: '#/components/schemas/TransmittalTemplateSelectListResponse' }
      required: [result]

    Result_TransmittalReadListResponseDto:
      type: object
      properties:
        result:
          type: array
          items: { $ref: '#/components/schemas/TransmittalReadListResponseDto' }
      required: [result]

    Result_TransmittalGetByBundleIdsResponse:
      type: object
      properties:
        result:
          type: array
          items: { $ref: '#/components/schemas/TransmittalGetByBundleIdsResponse' }
      required: [result]

    # --- Transmittals -----------------------------------------------------
    TransmittalCreateRequestDto:
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 255 }
        company_id: { type: integer, minimum: 0, maximum: 18446744073709551615 }
        additional_information: { type: string, nullable: true }
        resource_id: { type: string, format: uuid }
        auto_send: { type: boolean }
        documents_to_approve:
          type: array
          description: Не менее одного документа
          minItems: 1
          uniqueItems: true
          items: { $ref: '#/components/schemas/DocumentVersionRequestDto' }
        receivers: { $ref: '#/components/schemas/ReceiversRequestDto' }
        deadline_at:
          type: string
          format: date-time
          nullable: true
          description: Дедлайн в будущем
      required: [name, company_id, additional_information, resource_id, auto_send, documents_to_approve, receivers, deadline_at]

    TransmittalCreateResponseDto:
      type: object
      properties:
        id: { type: string, format: uuid }
      required: [id]

    TransmittalListRequestDto:
      type: object
      properties:
        limit: { type: integer, default: 1000 }
        bookmark: { type: string, nullable: true }
        resource_id: { type: string, format: uuid }
      required: [resource_id]

    TransmittalReadListResponseDto:
      type: object
      properties:
        id: { type: string, format: uuid }
        resource_id: { type: string, format: uuid }
        created_by: { type: integer }
        current_step_id: { type: integer }
        "no": { type: integer }
        name: { type: string }
        additional_information: { type: string, nullable: true }
        steps: { type: array, items: { type: integer } }
        auto_send: { type: boolean }
        receivers: { $ref: '#/components/schemas/ReceiversReadResponseDto' }
        deadline_at: { type: string, format: date-time, nullable: true }
        status: { $ref: '#/components/schemas/StatusReadResponseDto' }
        step: { $ref: '#/components/schemas/StepReadResponseDto' }
        sent_at: { type: string, format: date-time, nullable: true }
        received_at: { type: string, format: date-time, nullable: true }
        act_available: { type: boolean }
        reviews:
          type: array
          nullable: true
          items: { $ref: '#/components/schemas/ReviewLightResponse' }
      required: [id, resource_id, created_by, current_step_id, "no", name, additional_information, steps, auto_send, receivers, deadline_at, status, step, sent_at, received_at, act_available]

    TransmittalReadResponseDto:
      type: object
      properties:
        id: { type: string, format: uuid }
        resource_id: { type: string, format: uuid }
        created_by: { type: integer }
        current_step_id: { type: integer }
        "no": { type: integer }
        name: { type: string }
        additional_information: { type: string, nullable: true }
        steps: { type: array, items: { type: integer } }
        auto_send: { type: boolean }
        receivers: { $ref: '#/components/schemas/ReceiversReadResponseDto' }
        deadline_at: { type: string, format: date-time, nullable: true }
        sent_at: { type: string, format: date-time, nullable: true }
        received_at: { type: string, format: date-time, nullable: true }
        act_available: { type: boolean }
        status: { $ref: '#/components/schemas/StatusReadResponseDto' }
        step: { $ref: '#/components/schemas/StepReadResponseDto' }
        documents_to_approve:
          type: array
          items: { $ref: '#/components/schemas/DocumentVersionReadResponse' }
        history:
          type: array
          items: { $ref: '#/components/schemas/UserActionReadResponse' }
      required: [id, resource_id, created_by, current_step_id, "no", name, additional_information, steps, auto_send, receivers, deadline_at, sent_at, received_at, act_available, status, step, documents_to_approve, history]

    TransmittalStatusesStatResponseDto:
      type: object
      properties:
        status: { $ref: '#/components/schemas/StatusReadResponseDto' }
        count: { type: integer }
      required: [status, count]

    TransmittalListResponseDto:
      type: object
      properties:
        page: { $ref: '#/components/schemas/Page_TransmittalReadListResponseDto' }
        statuses_stat:
          type: array
          items: { $ref: '#/components/schemas/TransmittalStatusesStatResponseDto' }
      required: [page, statuses_stat]

    TransmittalGlobalStatusesStatResponseDto:
      type: object
      properties:
        resource_id: { type: string, format: uuid }
        statuses:
          type: array
          items: { $ref: '#/components/schemas/TransmittalStatusesStatResponseDto' }
      required: [resource_id, statuses]

    TransmittalGetGlobalStatusesRequestDto:
      type: object
      properties:
        resource_ids:
          type: array
          items: { type: string, format: uuid }
      required: [resource_ids]

    TransmittalGetGlobalStatusesResponseDto:
      type: object
      properties:
        result:
          type: array
          items: { $ref: '#/components/schemas/TransmittalGlobalStatusesStatResponseDto' }
      required: [result]

    TransmittalDownloadActResponse:
      type: object
      properties:
        presigned_url: { type: string }
      required: [presigned_url]

    TransmittalGetActReadinessResponse:
      type: object
      properties:
        act_available: { type: boolean }
      required: [act_available]

    TransmittalOpenCountResponseDto:
      type: object
      properties:
        count: { type: integer }
      required: [count]

    TransmittalLinkReviewRequest:
      type: object
      properties:
        review_id: { type: integer }
      required: [review_id]

    TransmittalSearchFilterRequestDto:
      type: object
      description: Фильтр поиска в рамках одного ресурса
      properties:
        q: { type: string, nullable: true }
        resource_id: { type: string, format: uuid }
        status_slug: { type: array, items: { type: string } }
        created_by: { type: array, items: { type: integer } }
        receivers:
          nullable: true
          allOf: [{ $ref: '#/components/schemas/ReceiversFilterDto' }]
        created_from: { type: string, format: date-time, nullable: true }
        created_to: { type: string, format: date-time, nullable: true }
        received_from: { type: string, format: date-time, nullable: true }
        received_to: { type: string, format: date-time, nullable: true }
        deadline_at: { type: string, format: date-time, nullable: true }
        unlimited: { type: boolean, nullable: true, default: false }
        auto_send: { type: array, items: { type: boolean } }
        limit: { type: integer, nullable: true, default: 100 }
        bookmark: { type: string, nullable: true }
      required: [resource_id]

    TransmittalSearchFilterDto:
      type: object
      description: Фильтр поиска по нескольким ресурсам
      properties:
        q: { type: string, nullable: true }
        resource_ids: { type: array, items: { type: string, format: uuid } }
        status_slug: { type: array, items: { type: string } }
        created_by: { type: array, items: { type: integer } }
        receivers:
          nullable: true
          allOf: [{ $ref: '#/components/schemas/ReceiversFilterDto' }]
        created_from: { type: string, format: date-time, nullable: true }
        created_to: { type: string, format: date-time, nullable: true }
        received_from: { type: string, format: date-time, nullable: true }
        received_to: { type: string, format: date-time, nullable: true }
        deadline_at: { type: string, format: date-time, nullable: true }
        unlimited: { type: boolean, nullable: true, default: false }
        auto_send: { type: array, items: { type: boolean } }
        limit: { type: integer, nullable: true, default: 100 }
        bookmark: { type: string, nullable: true }

    # --- Internal ---------------------------------------------------------
    TransmittalListInternalRequest:
      type: object
      properties:
        limit: { type: integer, default: 100 }
        transmittal_ids: { type: array, items: { type: string, format: uuid } }
        company_id: { type: integer, nullable: true }
        resource_id: { type: string, format: uuid, nullable: true }

    TransmittalGetByBundleIdsRequest:
      type: object
      properties:
        bundle_ids: { type: array, items: { type: string, format: uuid } }
      required: [bundle_ids]

    TransmittalGetByBundleIdsReadDto:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        status: { $ref: '#/components/schemas/StatusReadResponseDto' }
        created_at: { type: string, format: date-time }
      required: [id, name, status, created_at]

    TransmittalGetByBundleIdsResponse:
      type: object
      properties:
        bundle_id: { type: string, format: uuid }
        transmittals:
          type: array
          items: { $ref: '#/components/schemas/TransmittalGetByBundleIdsReadDto' }
      required: [bundle_id, transmittals]

    # --- Templates --------------------------------------------------------
    TransmittalTemplateListFiltersRequest:
      type: object
      properties:
        include_wo_resources: { type: boolean, default: true }
        resources: { type: array, nullable: true, items: { type: string, format: uuid } }
        companies: { type: array, nullable: true, items: { type: integer } }
        q: { type: string, nullable: true }
        bookmark: { type: string, nullable: true }
        limit: { type: integer, default: 20 }

    TransmittalTemplateListResponse:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        is_active: { type: boolean }
        resource_id: { type: string, format: uuid, nullable: true }
        allowed_initiators:
          nullable: true
          allOf: [{ $ref: '#/components/schemas/ReceiversReadResponseDto' }]
        created_by: { type: integer }
        created_at: { type: string, format: date-time }
        has_deadline_interval: { type: boolean }
        deadline_interval: { type: integer, minimum: 1, nullable: true, description: Интервал дедлайна в днях }
        auto_send: { type: boolean, nullable: true }
        has_additional_information: { type: boolean }
        additional_information: { type: string, nullable: true }
        receivers:
          nullable: true
          allOf: [{ $ref: '#/components/schemas/ReceiversReadResponseDto' }]
        invalid_receivers:
          nullable: true
          allOf: [{ $ref: '#/components/schemas/ReceiversReadResponseDto' }]
      required: [id, name, is_active, resource_id, allowed_initiators, created_by, created_at, has_deadline_interval, deadline_interval, auto_send, has_additional_information, additional_information, receivers, invalid_receivers]

    TransmittalTemplateCreateRequestDto:
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 255 }
        resource_id: { type: string, format: uuid, nullable: true }
        allowed_initiators:
          nullable: true
          allOf: [{ $ref: '#/components/schemas/ReceiversRequestDto' }]
        company_id: { type: integer }
        has_deadline_interval: { type: boolean }
        deadline_interval: { type: integer, minimum: 1, nullable: true, description: Интервал дедлайна в днях }
        auto_send: { type: boolean, nullable: true }
        has_additional_information: { type: boolean }
        additional_information: { type: string, nullable: true }
        receivers:
          nullable: true
          allOf: [{ $ref: '#/components/schemas/ReceiversRequestDto' }]
      required: [name, resource_id, allowed_initiators, company_id, has_deadline_interval, deadline_interval, auto_send, has_additional_information, additional_information, receivers]

    TransmittalTemplateCreateResponseDto:
      type: object
      properties:
        id: { type: string, format: uuid }
      required: [id]

    TransmittalTemplateSelectListResponse:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        created_by: { type: integer }
      required: [id, name, created_by]

    TransmittalTemplateGetSingleResponse:
      type: object
      properties:
        name: { type: string }
        created_by: { type: integer }
        has_deadline_interval: { type: boolean }
        deadline_interval: { type: integer, minimum: 1, nullable: true, description: Интервал дедлайна в днях }
        auto_send: { type: boolean, nullable: true }
        has_additional_information: { type: boolean }
        additional_information: { type: string, nullable: true }
        receivers:
          nullable: true
          allOf: [{ $ref: '#/components/schemas/ReceiversReadResponseDto' }]
        invalid_receivers:
          nullable: true
          allOf: [{ $ref: '#/components/schemas/ReceiversReadResponseDto' }]
      required: [name, created_by, has_deadline_interval, deadline_interval, auto_send, has_additional_information, additional_information, receivers, invalid_receivers]

    TransmittalTemplateUpdateRequest:
      type: object
      description: Частичное обновление. Все поля опциональны.
      properties:
        name: { type: string, minLength: 1, maxLength: 255, nullable: true }
        is_active: { type: boolean, nullable: true }
        resource_id: { type: string, format: uuid, nullable: true }
        allowed_initiators:
          nullable: true
          allOf: [{ $ref: '#/components/schemas/ReceiversRequestDto' }]
        has_deadline_interval: { type: boolean, nullable: true }
        deadline_interval: { type: integer, minimum: 1, nullable: true, description: Интервал дедлайна в днях }
        auto_send: { type: boolean, nullable: true }
        has_additional_information: { type: boolean, nullable: true }
        additional_information: { type: string, nullable: true }
        receivers:
          nullable: true
          allOf: [{ $ref: '#/components/schemas/ReceiversRequestDto' }]

    # --- Infra ------------------------------------------------------------
    ResourceStatus:
      type: object
      properties:
        name: { type: string }
        available: { type: boolean }
      required: [name, available]

    HealthCheckResponse:
      type: object
      properties:
        availability:
          type: array
          items: { $ref: '#/components/schemas/ResourceStatus' }
        status:
          type: string
          enum: [healthy, partially_healthy]
          description: Вычисляемое поле — `healthy`, если доступны все зависимости
          readOnly: true
      required: [availability, status]
