openapi: 3.0.3

info:
  title: Checklists
  version: "0.1.0"
  description: |
    REST API сервиса **checklists-backend** — управление чек-листами
    (`Checklist`) и их результатами (`ChecklistResult`).

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

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

    Интерактивная документация (ReDoc) доступна по `/docs/`, схема —
    по `/openapi.json/` (с учётом `root_path`). Админ-панель Piccolo монтируется
    по `/admin/` при `HTTP_APP_ADMIN_ENABLE=true`.

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

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

    В обоих режимах разбор выполняется с `verify_signature=False` — подпись на
    уровне приложения не проверяется, доверие обеспечивается сетевым слоем.
    Внутренние эндпоинты (`/internal/*`) и пути `/docs/`, `/openapi.json/`,
    `/admin/*` аутентификацию пропускают. При `JWT_AUTH_ENABLE=false` middleware
    не подключается и используется дефолтный пользователь.

    ### Пагинация
    Списочные ответы используют пагинацию limit/offset и оборачиваются в
    `PaginatedResponse` — `{ count, result }`, где `count` — число объектов в
    текущем ответе, `result` — сами объекты. Параметры: `limit` (по умолчанию
    `100`), `offset` (по умолчанию `0`), сортировка — `order_by`/`ascending`.

    ### Обработка ошибок
    Доменные ошибки (`controller/http/errors.py`) возвращаются как
    `application/json` с телом `{ "detail": "<текст>" }`. Маппинг:

    - `ResourceNotPermittedError` → **403**;
    - `ResourceNotFoundError` → **404**;
    - `StateConflictError` → **409**;
    - `ChecklistResultValidationError` → **422**;
    - прочее → **500**.

    Ошибки валидации тела/параметров запроса (Pydantic) отдаются FastAPI в
    стандартном формате `422` (`HTTPValidationError`). Все публичные эндпоинты
    (`/api/*`) при невалидном/отсутствующем токене возвращают `401`.

  contact:
    name: checklists-backend
    url: https://gitlab/proc/checklists-backend

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

tags:
  - name: Checklists
    description: Чек-листы — создание, просмотр, поиск, удаление
  - name: Checklist results
    description: Результаты чек-листов — создание, просмотр, обновление, удаление
  - name: internal
    description: Внутренние эндпоинты (только внутри кластера)

security:
  - bearerAuth: []

paths:
  # ==========================================================================
  # Checklists
  # ==========================================================================
  /api/v1/checklists/:
    post:
      tags: [Checklists]
      summary: Создать чек-лист
      operationId: createChecklist
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChecklistCreate'
      responses:
        '201':
          description: Созданный чек-лист
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChecklistReadFull'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
    get:
      tags: [Checklists]
      summary: Список чек-листов
      operationId: listChecklists
      parameters:
        - { $ref: '#/components/parameters/OrderByChecklist' }
        - { $ref: '#/components/parameters/Ascending' }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
        - name: company_id
          in: query
          required: false
          description: Фильтр по ID компаний
          schema:
            type: array
            nullable: true
            items:
              type: integer
              minimum: 1
      responses:
        '200':
          description: Страница чек-листов (компактное представление)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedResponse_ChecklistReadCompact'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/checklists/filter/:
    post:
      tags: [Checklists]
      summary: Список чек-листов (фильтры в теле)
      description: Аналог `GET /api/v1/checklists/`, но фильтры и пагинация передаются в теле запроса.
      operationId: filterChecklists
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChecklistFiltersWithPagination'
      responses:
        '200':
          description: Страница чек-листов (компактное представление)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedResponse_ChecklistReadCompact'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/checklists/{instance_id}/:
    get:
      tags: [Checklists]
      summary: Чек-лист по id
      operationId: getChecklist
      parameters:
        - { $ref: '#/components/parameters/InstanceId' }
      responses:
        '200':
          description: Чек-лист (полное представление)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChecklistReadFull'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      tags: [Checklists]
      summary: Удалить чек-лист
      operationId: deleteChecklist
      parameters:
        - { $ref: '#/components/parameters/InstanceId' }
      responses:
        '204':
          description: Чек-лист удалён
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  # ==========================================================================
  # Checklist results
  # ==========================================================================
  /api/v1/results/:
    post:
      tags: [Checklist results]
      summary: Создать результат чек-листа
      description: |
        Создаёт результат по `checklist_id`. Данные чек-листа переносятся бэком
        автоматически; в теле передаются метаданные и список значений инпутов
        (`input_values`).
      operationId: createChecklistResult
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChecklistResultCreate'
      responses:
        '201':
          description: Созданный результат
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChecklistResultRead'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
    get:
      tags: [Checklist results]
      summary: Список результатов чек-листов
      operationId: listChecklistResults
      parameters:
        - { $ref: '#/components/parameters/OrderByChecklistResult' }
        - { $ref: '#/components/parameters/Ascending' }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
        - { $ref: '#/components/parameters/FilterResultId' }
        - { $ref: '#/components/parameters/FilterChecklistId' }
        - { $ref: '#/components/parameters/FilterCreatorId' }
        - { $ref: '#/components/parameters/FilterIsDraft' }
        - { $ref: '#/components/parameters/FilterIsLocked' }
      responses:
        '200':
          description: Страница результатов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedResponse_ChecklistResultRead'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/results/{instance_id}/:
    get:
      tags: [Checklist results]
      summary: Результат чек-листа по id
      operationId: getChecklistResult
      parameters:
        - { $ref: '#/components/parameters/InstanceId' }
      responses:
        '200':
          description: Результат чек-листа
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChecklistResultRead'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    patch:
      tags: [Checklist results]
      summary: Обновить результат чек-листа
      description: Частичное обновление значений инпутов и флага черновика.
      operationId: updateChecklistResult
      parameters:
        - { $ref: '#/components/parameters/InstanceId' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChecklistResultPartialUpdate'
      responses:
        '200':
          description: Обновлённый результат
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChecklistResultRead'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      tags: [Checklist results]
      summary: Удалить результат чек-листа
      operationId: deleteChecklistResult
      parameters:
        - { $ref: '#/components/parameters/InstanceId' }
      responses:
        '204':
          description: Результат удалён
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  # ==========================================================================
  # Internal
  # ==========================================================================
  /internal/v1/results/:
    get:
      tags: [internal]
      summary: Список результатов (внутренний)
      description: Внутрикластерный эндпоинт. Аутентификация на уровне приложения не выполняется.
      operationId: internalListChecklistResults
      security: []
      parameters:
        - { $ref: '#/components/parameters/OrderByChecklistResult' }
        - { $ref: '#/components/parameters/Ascending' }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
        - { $ref: '#/components/parameters/FilterResultId' }
        - { $ref: '#/components/parameters/FilterChecklistId' }
        - { $ref: '#/components/parameters/FilterCreatorId' }
        - { $ref: '#/components/parameters/FilterIsDraft' }
        - { $ref: '#/components/parameters/FilterIsLocked' }
      responses:
        '200':
          description: Страница результатов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedResponse_ChecklistResultRead'
        '422': { $ref: '#/components/responses/ValidationError' }

  /internal/v1/results/lock:
    patch:
      tags: [internal]
      summary: Массовое обновление блокировки результатов
      description: Устанавливает флаг `is_locked` для списка результатов по их id. Внутрикластерный эндпоинт.
      operationId: internalBulkUpdateLock
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChecklistResultBulkLockingPartialUpdate'
      responses:
        '204':
          description: Флаги блокировки обновлены
        '422': { $ref: '#/components/responses/ValidationError' }

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

  parameters:
    InstanceId:
      name: instance_id
      in: path
      required: true
      schema:
        type: integer
    Limit:
      name: limit
      in: query
      required: false
      description: Максимальное количество объектов
      schema:
        type: integer
        default: 100
    Offset:
      name: offset
      in: query
      required: false
      description: Количество пропущенных объектов
      schema:
        type: integer
        default: 0
    Ascending:
      name: ascending
      in: query
      required: false
      description: Сортировка по возрастанию
      schema:
        type: boolean
        default: true
    OrderByChecklist:
      name: order_by
      in: query
      required: false
      description: Поле для сортировки
      schema:
        type: string
        enum: [id]
        default: id
    OrderByChecklistResult:
      name: order_by
      in: query
      required: false
      description: Поле для сортировки
      schema:
        type: string
        enum: [id]
        default: id
    FilterResultId:
      name: id
      in: query
      required: false
      description: Фильтр по ID результата
      schema:
        type: array
        nullable: true
        items:
          type: integer
          minimum: 1
    FilterChecklistId:
      name: checklist_id
      in: query
      required: false
      description: Фильтр по ID чек-листа
      schema:
        type: array
        nullable: true
        items:
          type: integer
          minimum: 1
    FilterCreatorId:
      name: creator_id
      in: query
      required: false
      description: Фильтр по ID создателя результата
      schema:
        type: array
        nullable: true
        items:
          type: integer
          minimum: 1
    FilterIsDraft:
      name: is_draft
      in: query
      required: false
      description: Признак «чернового» результата
      schema:
        type: boolean
        nullable: true
    FilterIsLocked:
      name: is_locked
      in: query
      required: false
      description: Признак блокировки результата
      schema:
        type: boolean
        nullable: true

  responses:
    Unauthorized:
      description: Токен не предоставлен или невалиден
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/HTTPError'
    Forbidden:
      description: Недостаточно прав
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/HTTPError'
    NotFound:
      description: Запрошенный ресурс не найден
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/HTTPError'
    Conflict:
      description: Конфликт состояния
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/HTTPError'
    ValidationError:
      description: |
        Ошибка валидации тела/параметров запроса (FastAPI/Pydantic) либо
        доменная ошибка валидации результата (`{ detail }`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/HTTPValidationError'

  schemas:
    # ---- Общие ----
    HTTPError:
      type: object
      properties:
        detail:
          type: string
          description: Описание ошибки
      required: [detail]

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

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

    # ---- Ограничения инпутов ----
    InputChoiceOption:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Уникальный идентификатор опции
        name:
          type: string
          description: Название опции
          example: "Да"
        order:
          type: integer
          description: Порядковый номер опции
          example: 1
        color:
          type: string
          nullable: true
          description: Цвет опции (hex или именованный html-цвет)
          example: "#00aa00"
        selected_tip:
          type: string
          nullable: true
          description: Заметка при выборе опции
          example: 'Рекомендуется добавить комментарий при ответе "Нет"'
        alt_name:
          type: string
          nullable: true
          description: Название опции для записи в историю
          example: "Одобрено"
      required: [id, name, order, selected_tip]

    InputChoiceConstraints:
      type: object
      properties:
        type:
          type: string
          enum: [choice]
        options:
          type: array
          nullable: true
          description: Список опций для выбора
          items:
            $ref: '#/components/schemas/InputChoiceOption'
      required: [type, options]

    InputStringConstraints:
      type: object
      properties:
        type:
          type: string
          enum: [string]
        min_length:
          type: integer
          minimum: 0
          default: 0
          description: Минимально допустимое количество символов
        max_length:
          type: integer
          minimum: 1
          default: 1000
          description: Максимально допустимое количество символов
      required: [type]

    InputConstraints:
      oneOf:
        - $ref: '#/components/schemas/InputChoiceConstraints'
        - $ref: '#/components/schemas/InputStringConstraints'
      discriminator:
        propertyName: type
        mapping:
          choice: '#/components/schemas/InputChoiceConstraints'
          string: '#/components/schemas/InputStringConstraints'

    # ---- Чек-лист (создание) ----
    ChecklistInputCreate:
      type: object
      properties:
        name:
          type: string
          maxLength: 1024
          description: Название
          example: "Комментарий"
        order:
          type: integer
          description: Порядковый номер
        is_required:
          type: boolean
          description: Обязательное ли поле для заполнения
        constraints:
          $ref: '#/components/schemas/InputConstraints'
      required: [name, order, is_required, constraints]

    ChecklistItemCreate:
      type: object
      properties:
        description:
          type: string
          maxLength: 8192
          description: Описание шага
        order:
          type: integer
          description: Порядковый номер
        inputs:
          type: array
          description: Список элементов ввода
          items:
            $ref: '#/components/schemas/ChecklistInputCreate'
      required: [description, order, inputs]

    ChecklistCreate:
      type: object
      properties:
        name:
          type: string
          maxLength: 250
          description: Название
          example: "Чек-лист проверки документов"
        description:
          type: string
          maxLength: 8192
          description: Описание чек-листа
        company_id:
          type: integer
          minimum: 1
          description: ID компании
          example: 1
        items:
          type: array
          description: Список шагов чек-листа
          items:
            $ref: '#/components/schemas/ChecklistItemCreate'
      required: [name, description, company_id, items]

    # ---- Чек-лист (чтение) ----
    ChecklistInputRead:
      type: object
      properties:
        id:
          type: integer
          example: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        name:
          type: string
          maxLength: 1024
        order:
          type: integer
        is_required:
          type: boolean
        constraints:
          $ref: '#/components/schemas/InputConstraints'
      required: [id, created_at, updated_at, name, order, is_required, constraints]

    ChecklistItemRead:
      type: object
      properties:
        id:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        description:
          type: string
          maxLength: 8192
        order:
          type: integer
        inputs:
          type: array
          description: Список элементов ввода
          items:
            $ref: '#/components/schemas/ChecklistInputRead'
      required: [id, created_at, updated_at, description, order, inputs]

    ChecklistReadCompact:
      type: object
      properties:
        id:
          type: integer
          example: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        name:
          type: string
        description:
          type: string
        company_id:
          type: integer
          minimum: 1
      required: [id, created_at, updated_at, name, description, company_id]

    ChecklistReadFull:
      allOf:
        - $ref: '#/components/schemas/ChecklistReadCompact'
        - type: object
          properties:
            items:
              type: array
              description: Список шагов чек-листа
              items:
                $ref: '#/components/schemas/ChecklistItemRead'
          required: [items]

    ChecklistFiltersWithPagination:
      type: object
      properties:
        order_by:
          type: string
          enum: [id]
          default: id
        ascending:
          type: boolean
          default: true
        limit:
          type: integer
          default: 100
        offset:
          type: integer
          default: 0
        company_id:
          type: array
          nullable: true
          description: Фильтр по ID компаний
          items:
            type: integer
            minimum: 1

    # ---- Результаты чек-листов ----
    ChecklistResultInputCreate:
      type: object
      properties:
        input_id:
          type: integer
          description: ID инпута, для которого устанавливается значение
          example: 1
        value:
          description: 'Значение (тип зависит от инпута: id опции для choice, строка для string)'
          nullable: true
          example: "Да"
      required: [input_id, value]

    ChecklistResultCreate:
      type: object
      properties:
        entity_type:
          type: string
          description: Сущность, для которой создан результат
          example: "review"
        entity_id:
          type: string
          description: ID сущности, для которой создан результат
          example: "1"
        checklist_id:
          type: integer
          description: ID чек-листа
          example: 1
        accessible_by:
          type: array
          description: SA ID роли/места/пользователя, которым доступен результат
          items:
            type: string
            format: uuid
        is_draft:
          type: boolean
          description: Является ли результат черновым
        input_values:
          type: array
          description: Список устанавливаемых значений
          items:
            $ref: '#/components/schemas/ChecklistResultInputCreate'
      required: [entity_type, entity_id, checklist_id, accessible_by, is_draft, input_values]

    ChecklistResultPartialUpdate:
      type: object
      properties:
        input_values:
          type: array
          description: Список устанавливаемых значений
          items:
            $ref: '#/components/schemas/ChecklistResultInputCreate'
        is_draft:
          type: boolean
          description: Является ли результат черновым
      required: [input_values, is_draft]

    ChecklistResultInput:
      type: object
      properties:
        input_id:
          type: integer
          description: ID инпута, для которого создан результат
        name:
          type: string
          description: Название
        order:
          type: integer
          description: Порядковый номер
        value:
          type: string
          nullable: true
          description: Введённое значение (строка или UUID выбранной опции)
        value_text:
          type: string
          nullable: true
          description: Текстовое представление введённого значения
        color:
          type: string
          nullable: true
          description: Цвет значения
      required: [input_id, name, order, value, value_text, color]

    ChecklistResultItem:
      type: object
      properties:
        description:
          type: string
          description: Описание шага
        order:
          type: integer
          description: Порядковый номер
        inputs:
          type: array
          description: Список введённых значений
          items:
            $ref: '#/components/schemas/ChecklistResultInput'
      required: [description, order, inputs]

    ChecklistResultRead:
      type: object
      properties:
        id:
          type: integer
          example: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        entity_type:
          type: string
          example: "review"
        entity_id:
          type: string
          example: "1"
        checklist_id:
          type: integer
        accessible_by:
          type: array
          items:
            type: string
            format: uuid
        is_draft:
          type: boolean
        company_id:
          type: integer
          description: ID компании
        is_locked:
          type: boolean
          description: Заблокирован ли результат для изменений
        items:
          type: array
          description: Список шагов чек-листа
          items:
            $ref: '#/components/schemas/ChecklistResultItem'
      required:
        - id
        - created_at
        - updated_at
        - entity_type
        - entity_id
        - checklist_id
        - accessible_by
        - is_draft
        - company_id
        - is_locked
        - items

    ChecklistResultBulkLockingPartialUpdate:
      type: object
      properties:
        ids:
          type: array
          description: Список id результатов, которым нужно обновить флаг
          items:
            type: integer
          example: [1, 2, 3]
        is_locked:
          type: boolean
          description: Заблокирован ли результат для изменений
      required: [ids, is_locked]

    # ---- Пагинация ----
    PaginatedResponse_ChecklistReadCompact:
      type: object
      properties:
        count:
          type: integer
          description: Количество объектов
          example: 1
        result:
          type: array
          description: Объекты
          items:
            $ref: '#/components/schemas/ChecklistReadCompact'
      required: [count, result]

    PaginatedResponse_ChecklistResultRead:
      type: object
      properties:
        count:
          type: integer
          description: Количество объектов
          example: 1
        result:
          type: array
          description: Объекты
          items:
            $ref: '#/components/schemas/ChecklistResultRead'
      required: [count, result]
