openapi: 3.0.3

info:
  title: Comparisons Service API
  version: "1.0.0"
  description: |
    REST API сервиса **comparisons-backend** (`pdm/comparisons-backend`) — создание и
    просмотр сравнений (облако-BIM отклонение/статусы, облако-облако, облако-поверхность,
    pdf-pdf), их элементов, изменений и фильтров.

    Сервис написан на Go (роутер **gorilla/mux**). Сервер собирается функцией
    `bootstrapAPI` в `cmd/api/bootstrap.go`. Роутинг состоит из двух групп:

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

    ### Аутентификация
    Публичные эндпоинты (`/api/v1/*`) требуют JWT: middleware `auth.JWTToCtx` +
    `auth.JWTUserExtractorFromCtx` (`gitlab.com/sarex-team/gotools/auth`). Токен
    передаётся заголовком `Authorization: Bearer <jwt>`; middleware
    `auth.DeleteJWTFromQueryMiddleware` дополнительно позволяет передать токен
    query-параметром `jwt` (он удаляется из запроса после разбора).

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

    ### Обработка ошибок
    Ошибки возвращаются функцией `httperror.Write` (`gotools/httperror`). Основные
    статусы: `400` — некорректный запрос/параметры, `404` — сравнение не найдено,
    `500` — внутренняя ошибка. Идентификатор запроса пробрасывается middleware
    `reqid`.

    ### Замечания (расхождения кода)
    - `GET /api/v1/comparisons` возвращает данные разной формы в зависимости от
      query-параметра: `workspace_id` → `{ comparisons: [] }`, `document_id` →
      `{ results: [] }`, `bundle_id` → одиночный объект сравнения.
    - `GET /api/v1/filter_fields` и `GET /api/v1/elements` работают только со
      сравнениями типа `deviation`; для других типов вернётся `400`.
    - Схема сравнения полиморфна по полю `type` (`deviation`/`c2c`/`c2s`/`abap`/`pdf2pdf`).

  contact:
    name: comparisons-backend
    url: https://gitlab.com/sarex-team/comparisons-backend

servers:
  - url: https://api.sarex.io/comparisons
    description: Production (ingress)
  - url: https://stage-api.sarex.io/comparisons
    description: Stage (ingress)
  - url: https://api.preprod.sarex.io/comparisons
    description: Preprod (ingress)
  - url: http://comparisons-backend-service.comparisons-stage
    description: Внутрикластерный адрес (ClusterIP). Единственный способ достучаться до /internal/v1
  - url: http://localhost:8080
    description: Локальный запуск (API_ADDRESS по умолчанию 0.0.0.0:8080)

tags:
  - name: types
    description: Справочник типов сравнений
  - name: comparisons
    description: Сравнения — создание, просмотр, удаление
  - name: elements
    description: Элементы сравнения и их изменения
  - name: internal
    description: Внутренние эндпоинты (только внутри кластера)

security:
  - bearerAuth: []

paths:
  # ==========================================================================
  # Types
  # ==========================================================================
  /api/v1/types:
    get:
      tags: [types]
      summary: Справочник типов сравнений
      description: Возвращает доступные типы сравнений и их параметры (для построения форм).
      operationId: getTypes
      responses:
        '200':
          description: Список типов сравнений
          content:
            application/json:
              schema:
                type: object
                properties:
                  types:
                    type: array
                    items:
                      $ref: '#/components/schemas/CompareType'

  # ==========================================================================
  # Comparisons
  # ==========================================================================
  /api/v1/comparisons:
    post:
      tags: [comparisons]
      summary: Создать сравнение
      description: |
        Создаёт сравнение указанного типа. Автор берётся из JWT. Если `parent_doc_id`
        и `parent_disk_id` не заданы — вычисляются по рабочей области.
      operationId: createComparison
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRequest'
      responses:
        '200':
          description: Созданное сравнение
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Comparison'
        '400':
          description: Некорректный запрос / неизвестный тип сравнения
        '500':
          description: Внутренняя ошибка
    get:
      tags: [comparisons]
      summary: Список сравнений
      description: |
        Возвращает сравнения по одному из query-параметров. Форма ответа зависит
        от параметра (см. описание в `info`). Должен быть задан ровно один параметр.
      operationId: getComparisons
      parameters:
        - name: workspace_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: "Сравнения рабочей области → ответ `{ comparisons: [] }`"
        - name: bundle_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Сравнение по бандлу → ответ — одиночный объект
        - name: document_id
          in: query
          required: false
          schema:
            type: integer
            format: int64
          description: "Сравнения документа → ответ `{ results: [] }`"
      responses:
        '200':
          description: Результат (форма зависит от параметра запроса)
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    properties:
                      comparisons:
                        type: array
                        items:
                          $ref: '#/components/schemas/Comparison'
                  - type: object
                    properties:
                      results:
                        type: array
                        items:
                          $ref: '#/components/schemas/Comparison'
                  - $ref: '#/components/schemas/Comparison'
        '400':
          description: Не задан workspace_id / bundle_id / document_id
        '500':
          description: Внутренняя ошибка

  /api/v1/comparisons/{comparison_id}:
    parameters:
      - name: comparison_id
        in: path
        required: true
        schema:
          type: integer
          format: int64
    get:
      tags: [comparisons]
      summary: Сравнение по id
      operationId: getComparisonById
      responses:
        '200':
          description: Сравнение
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Comparison'
        '404':
          description: Сравнение не найдено
        '500':
          description: Внутренняя ошибка
    delete:
      tags: [comparisons]
      summary: Удалить сравнение
      description: Мягкое удаление сравнения. Автор берётся из JWT.
      operationId: deleteComparisonById
      responses:
        '200':
          description: Успешно удалено
        '400':
          description: Некорректный id
        '500':
          description: Внутренняя ошибка

  /api/v1/filter_fields:
    get:
      tags: [comparisons]
      summary: Поля фильтрации сравнения
      description: |
        Возвращает набор полей фильтрации для сравнения типа `deviation` по документу.
        Для полей отклонения (`deviation`, `deviation_x/y/z`) заполняются `min`/`max`.
        Если сравнение по документу не найдено — вернётся пустой `results`.
      operationId: getFilterFields
      parameters:
        - name: doc_id
          in: query
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Поля фильтрации
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/FilterFields'
        '400':
          description: Не задан doc_id / сравнение не типа deviation
        '500':
          description: Внутренняя ошибка

  /api/v1/tolerance:
    get:
      tags: [comparisons]
      summary: Допуск (tolerance) сравнения
      description: Возвращает значение допустимого отклонения для сравнения типа `deviation` по бандлу.
      operationId: getTolerance
      parameters:
        - name: bundle_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Допуск
          content:
            application/json:
              schema:
                type: object
                properties:
                  tolerance:
                    type: number
                    format: double
        '400':
          description: Не задан / некорректный bundle_id
        '404':
          description: Сравнение не найдено
        '500':
          description: Внутренняя ошибка

  # ==========================================================================
  # Elements
  # ==========================================================================
  /api/v1/elements:
    get:
      tags: [elements]
      summary: Список элементов сравнения
      description: |
        Постранично возвращает элементы сравнения типа `deviation` по документу.
        Параметры отклонения задаются строкой вида `min:max`. Если сравнение не
        найдено — вернётся пустой `results` с типами по умолчанию.
      operationId: getListElements
      parameters:
        - name: doc_id
          in: query
          required: true
          schema:
            type: integer
            format: int64
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 10
            minimum: 0
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            default: 0
            minimum: 0
        - name: order_by
          in: query
          required: false
          schema:
            type: string
            enum: [deviation, deviation_x, deviation_y, deviation_z, sarex_id, name]
            default: sarex_id
        - name: order
          in: query
          required: false
          schema:
            type: string
            enum: [asc, desc]
            default: asc
        - name: deviation
          in: query
          required: false
          schema:
            type: string
          description: Диапазон общего отклонения в формате `min:max`
        - name: deviation_x
          in: query
          required: false
          schema:
            type: string
          description: Диапазон отклонения по X в формате `min:max`
        - name: deviation_y
          in: query
          required: false
          schema:
            type: string
          description: Диапазон отклонения по Y в формате `min:max`
        - name: deviation_z
          in: query
          required: false
          schema:
            type: string
          description: Диапазон отклонения по Z в формате `min:max`
        - name: deviation_status
          in: query
          required: false
          schema:
            type: string
            enum: [in_tolerance, out_of_tolerance]
        - name: view_status
          in: query
          required: false
          schema:
            type: string
            enum: [viewed, not_viewed]
        - name: only_with_comment
          in: query
          required: false
          schema:
            type: boolean
      responses:
        '200':
          description: Страница элементов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListElementsResponse'
        '400':
          description: Некорректные параметры запроса
        '500':
          description: Внутренняя ошибка

  /api/v1/elements/{element_id}:
    parameters:
      - name: element_id
        in: path
        required: true
        schema:
          type: integer
          format: int64
    patch:
      tags: [elements]
      summary: Обновить элемент
      description: |
        Обновляет один из атрибутов элемента: комментарий, статус отклонения или
        статус просмотра. Должно быть задано ровно одно из полей. Изменение
        фиксируется записью в истории изменений.
      operationId: updateElement
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateElementRequest'
      responses:
        '200':
          description: Успешно обновлено (тело не возвращается)
        '400':
          description: Некорректный запрос
        '500':
          description: Внутренняя ошибка

  /api/v1/elements/{element_id}/changes:
    get:
      tags: [elements]
      summary: История изменений элемента
      operationId: getElementChanges
      parameters:
        - name: element_id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Список изменений
          content:
            application/json:
              schema:
                type: object
                properties:
                  changes:
                    type: array
                    items:
                      $ref: '#/components/schemas/Change'
        '400':
          description: Некорректный element_id
        '500':
          description: Внутренняя ошибка

  # ==========================================================================
  # Internal
  # ==========================================================================
  /internal/v1/comparisons/{comparison_id}/webhook:
    post:
      tags: [internal]
      summary: Webhook результата сравнения (внутренний)
      description: |
        Вызывается после завершения обработки сравнения типа `deviation`. Скачивает
        `deviation_json` из PDM-хранилища по бандлу, создаёт элементы сравнения и
        возвращает содержимое `deviation_json`. Аутентификация на уровне приложения
        не требуется.
      operationId: comparisonWebhook
      security: []
      parameters:
        - name: comparison_id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Содержимое deviation_json
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviationJSON'
        '404':
          description: Сравнение не найдено
        '500':
          description: Внутренняя ошибка

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT в заголовке `Authorization: Bearer <jwt>`. Допускается передача
        query-параметром `jwt` (удаляется middleware после разбора).

  schemas:
    ComparisonType:
      type: string
      enum: [c2c, c2s, deviation, abap, pdf2pdf]

    CompareType:
      type: object
      description: Тип сравнения и набор его параметров (для формы создания).
      properties:
        name:
          $ref: '#/components/schemas/ComparisonType'
        verbose_name:
          type: string
        params:
          type: array
          items:
            $ref: '#/components/schemas/CompareFormParam'

    CompareFormParam:
      type: object
      properties:
        name:
          type: string
        verbose_name:
          type: string
        hint:
          type: string
        source:
          type: string
          enum: [form, viewer]
        type:
          type: string
          description: Тип поля (напр. cloud, bimv2, number, multichoice, choice, array, pdf, surface)
        options:
          type: array
          items:
            type: object
            additionalProperties: true
        default: {}
        min:
          type: number
          format: double
        max:
          type: number
          format: double

    CreateRequest:
      type: object
      required: [name, workspace_id, type, company_id, params]
      properties:
        name:
          type: string
        workspace_id:
          type: string
          format: uuid
        parent_doc_id:
          type: integer
          format: int64
          nullable: true
        parent_disk_id:
          type: string
          format: uuid
          nullable: true
        type:
          $ref: '#/components/schemas/ComparisonType'
        company_id:
          type: integer
          format: int64
        params:
          type: object
          description: Параметры сравнения, зависят от type (см. /api/v1/types).
          additionalProperties: true

    Comparison:
      type: object
      description: |
        Полиморфное сравнение. Поле `type` определяет набор `params`. Ниже приведён
        пример для типа `deviation`; для других типов набор params отличается.
      properties:
        id:
          type: integer
          format: int64
        type:
          $ref: '#/components/schemas/ComparisonType'
        created_by:
          type: integer
          format: int64
        name:
          type: string
        workspace_id:
          type: string
          format: uuid
        workflow_id:
          type: string
          format: uuid
          nullable: true
        document_id:
          type: integer
          format: int64
          nullable: true
        bundle_id:
          type: string
          format: uuid
          nullable: true
        params:
          $ref: '#/components/schemas/DeviationParams'

    DeviationParams:
      type: object
      properties:
        cloud_bundle_id:
          type: string
          format: uuid
        model_bundle_id:
          type: string
          format: uuid
        down_sample_size:
          type: number
          format: double
        resolution:
          type: number
          format: double
        linear_margin:
          type: number
          format: double
        tolerance:
          type: number
          format: double
        sarex_ids:
          type: array
          items:
            type: integer
            format: int64
        transformation:
          type: array
          items:
            type: number
            format: double
        applied_statuses:
          type: array
          items:
            type: string
        crop_margin:
          type: number
          format: double
          nullable: true

    Element:
      type: object
      properties:
        id:
          type: integer
          format: int64
        sarex_id:
          type: integer
          format: int64
        name:
          type: string
        deviation_status:
          type: string
          enum: [in_tolerance, out_of_tolerance]
        view_status:
          type: string
          enum: [viewed, not_viewed]
        deviation:
          type: number
          format: double
        deviation_x:
          type: number
          format: double
        deviation_y:
          type: number
          format: double
        deviation_z:
          type: number
          format: double
        axis_and_angle:
          type: object
          description: Ось и угол поворота (utils.AxisAndAngle)
          additionalProperties: true
        comment:
          type: string
          nullable: true
        bbox_matrix:
          type: array
          items:
            type: number
            format: double
        reg_matrix:
          type: array
          items:
            type: number
            format: double

    ListElementsResponse:
      type: object
      properties:
        count:
          type: integer
        next:
          type: string
          nullable: true
        previous:
          type: string
          nullable: true
        types:
          type: object
          additionalProperties:
            type: array
            items:
              $ref: '#/components/schemas/Option'
        crop_margin:
          type: number
          format: double
          nullable: true
        results:
          type: array
          items:
            $ref: '#/components/schemas/Element'

    UpdateElementRequest:
      type: object
      description: |
        Должно быть задано ровно одно из полей (`comment`, `deviation_status`,
        `view_status`) — валидатор `required_without_all`.
      properties:
        comment:
          type: string
        deviation_status:
          type: string
          enum: [in_tolerance, out_of_tolerance]
        view_status:
          type: string
          enum: [viewed, not_viewed]

    Change:
      type: object
      properties:
        id:
          type: integer
          format: int64
        element_id:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
        author:
          type: integer
          format: int64
        changed_field:
          type: string
          enum: [deviation_status, view_status, comment]
        old_value:
          type: string
        new_value:
          type: string

    FilterFields:
      type: object
      properties:
        name:
          type: string
          enum: [deviation, deviation_x, deviation_y, deviation_z, deviation_status, view_status, only_with_comment]
        verbose_name:
          type: string
        type:
          type: string
          enum: [checkbox, selector, slider]
        options:
          type: array
          items:
            $ref: '#/components/schemas/Option'
        min:
          type: number
          format: double
          nullable: true
        max:
          type: number
          format: double
          nullable: true

    Option:
      type: object
      properties:
        name:
          type: string
        verbose_name:
          type: string

    DeviationJSON:
      type: object
      properties:
        crop_margin:
          type: number
          format: double
        nodes:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/Node'

    Node:
      type: object
      properties:
        bbox_matrix:
          type: array
          items:
            type: number
            format: double
        reg_matrix:
          type: array
          items:
            type: number
            format: double
        node_name:
          type: string
