openapi: 3.0.3

info:
  title: Contracts Service API
  version: "1.0.0"
  description: |
    REST API сервиса **contracts** (`platform/contracts`) — управление
    договорами (контрактами): создание (в т.ч. массовое), получение по id,
    списочный вывод с фильтрами и пагинацией, обновление.

    Сервис написан на Go (**Fiber v3**). Приложение собирается в
    `internal/app/http/app.go` (`New`). Роутинг вложен под общий префикс
    `/api/v0` (`app.server.Group("/api/v0")`), внутри — группа
    `/contracts` (`internal/controller/http/v0`).

    ### Аутентификация
    Все эндпоинты группы `/api/v0/contracts` защищены middleware
    (`internal/adapter/auth/middleware.go`). Поддерживаются два режима:

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

    Корневой эндпоинт `GET /api/v0/` (health/ping) аутентификации не требует.

    ### Идентификаторы
    Идентификатор договора — **ULID** (строка), парсится через
    `ulid.Parse`. `resource_id` — **UUID**.

    ### Пагинация
    Списочный вывод использует `limit`/`offset` (query-параметры, по умолчанию
    `limit=100`, `offset=0`).

    ### Обработка ошибок
    Ошибки возвращаются с соответствующим HTTP-статусом; тело — либо строка
    с описанием, либо `{"error": "..."}` (при внутренней панике). Коды:
    `400` — некорректный запрос/невалидный id, `401` — проблемы аутентификации,
    `403` — пользователь не состоит в компании (`tenant_id`), `404` — договор
    не найден, `500` — внутренняя ошибка, `501` — метод не реализован.

    ### Замечания (расхождения кода)
    - `PATCH` и `DELETE` (как по коллекции, так и по id) возвращают
      **`501 Not Implemented`** — обработчики-заглушки.
    - `POST /api/v0/contracts` принимает **как одиночный объект, так и массив**:
      тип создания выбирается по форме тела (объект → создание одного договора,
      массив → пакетное создание).
    - `GET /api/v0/contracts` (список) требует query-параметр `tenant_id`
      (middleware `UserInCompanyMiddleware` проверяет, что пользователь состоит
      в этой компании; иначе `400`/`403`).

servers:
  - url: https://api.sarex.io/contracts
    description: production
  - url: https://stage-api.sarex.io/contracts
    description: stage
  - url: https://api.preprod.sarex.io/contracts
    description: preprod

security:
  - bearerAuth: []

tags:
  - name: contracts
    description: Договоры
  - name: service
    description: Служебные эндпоинты

paths:
  /api/v0/:
    get:
      tags: [service]
      summary: Health / ping
      description: Возвращает `200 OK` без тела. Аутентификация не требуется.
      security: []
      responses:
        "200":
          description: OK

  /api/v0/contracts:
    post:
      tags: [contracts]
      summary: Создать договор или несколько договоров
      description: |
        Принимает либо одиночный объект `CreateContractRequest`, либо массив
        таких объектов. Форма тела определяет режим создания.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/CreateContractRequest"
                - type: array
                  items:
                    $ref: "#/components/schemas/CreateContractRequest"
      responses:
        "201":
          description: Договор(ы) создан(ы)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ContractResponse"
                  - type: array
                    items:
                      $ref: "#/components/schemas/ContractResponse"
        "400":
          description: Некорректное тело запроса / ошибка валидации
        "401":
          description: Ошибка аутентификации
        "500":
          description: Внутренняя ошибка
    get:
      tags: [contracts]
      summary: Список договоров
      description: |
        Возвращает договоры с фильтрацией и пагинацией. Требует `tenant_id`
        (проверяется принадлежность пользователя к компании).
      parameters:
        - name: tenant_id
          in: query
          required: true
          schema:
            type: integer
            format: int64
          description: Идентификатор компании/арендатора
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            format: int64
            default: 100
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            format: int64
            default: 0
        - name: resource_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: contractor_id
          in: query
          required: false
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: Список договоров
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContractPaginatedResponse"
        "400":
          description: Некорректные параметры запроса / отсутствует tenant_id
        "401":
          description: Ошибка аутентификации
        "403":
          description: Пользователь не состоит в указанной компании
        "500":
          description: Внутренняя ошибка
    put:
      tags: [contracts]
      summary: Пакетное обновление договоров
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/UpdateContractRequest"
      responses:
        "200":
          description: Договоры обновлены
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ContractResponse"
        "400":
          description: Некорректное тело запроса
        "401":
          description: Ошибка аутентификации
        "500":
          description: Внутренняя ошибка
    patch:
      tags: [contracts]
      summary: Пакетное частичное обновление (не реализовано)
      responses:
        "501":
          description: Not Implemented
    delete:
      tags: [contracts]
      summary: Пакетное удаление (не реализовано)
      responses:
        "501":
          description: Not Implemented

  /api/v0/contracts/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Идентификатор договора (ULID)
    get:
      tags: [contracts]
      summary: Получить договор по id
      responses:
        "200":
          description: Договор
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContractResponse"
        "400":
          description: Невалидный id
        "401":
          description: Ошибка аутентификации
        "404":
          description: Договор не найден
        "500":
          description: Внутренняя ошибка
    put:
      tags: [contracts]
      summary: Обновить договор
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateContractRequest"
      responses:
        "200":
          description: Договор обновлён
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContractResponse"
        "400":
          description: Невалидный id / некорректное тело
        "401":
          description: Ошибка аутентификации
        "404":
          description: Договор не найден
        "500":
          description: Внутренняя ошибка
    patch:
      tags: [contracts]
      summary: Частичное обновление (не реализовано)
      responses:
        "501":
          description: Not Implemented
    delete:
      tags: [contracts]
      summary: Удалить договор (не реализовано)
      responses:
        "501":
          description: Not Implemented

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Основной токен `Authorization: Bearer <jwt>` (проверяется по RSA-ключу).
        Опционально может передаваться заголовок `Identity: Bearer <jwt>`
        (режим Zitadel), который имеет приоритет.

  schemas:
    Contractor:
      type: object
      description: Произвольный JSON-объект с данными контрагента (в БД — JSONB).
      additionalProperties: true

    CreateContractRequest:
      type: object
      required: [number, tenant_id, started_at, deadline_at]
      properties:
        number:
          type: string
          minLength: 1
          description: Номер договора
        tenant_id:
          type: integer
          format: int64
          description: Идентификатор компании/арендатора
        resource_id:
          type: string
          format: uuid
          nullable: true
        contractor:
          $ref: "#/components/schemas/Contractor"
        started_at:
          type: string
          format: date-time
        deadline_at:
          type: string
          format: date-time
        cost:
          type: number
          format: double
          minimum: 0
        description:
          type: string

    UpdateContractRequest:
      type: object
      required: [number, tenant_id, started_at, deadline_at]
      properties:
        id:
          type: string
          description: ULID договора
        number:
          type: string
          minLength: 1
        tenant_id:
          type: integer
          format: int64
        resource_id:
          type: string
          format: uuid
          nullable: true
        contractor:
          $ref: "#/components/schemas/Contractor"
        started_at:
          type: string
          format: date-time
        deadline_at:
          type: string
          format: date-time
        cost:
          type: number
          format: double
          minimum: 0
        description:
          type: string

    ContractResponse:
      type: object
      properties:
        id:
          type: string
          description: ULID договора
        number:
          type: string
        tenant_id:
          type: integer
          format: int64
        resource_id:
          type: string
          format: uuid
          nullable: true
        contractor:
          $ref: "#/components/schemas/Contractor"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
        deadline_at:
          type: string
          format: date-time
        cost:
          type: number
          format: double
        description:
          type: string

    ContractPaginatedResponse:
      type: object
      properties:
        count:
          type: integer
          format: int64
        limit:
          type: integer
          format: int64
        offset:
          type: integer
          format: int64
        results:
          type: array
          items:
            $ref: "#/components/schemas/ContractResponse"
