openapi: 3.1.0
info:
  title: Найда (Nayda) — публичный API
  version: 1.0.0
  summary: B2B-поиск поставщиков и компаний РФ
  description: |
    Публичный срез API Найды для ИИ-агентов и интеграций: поиск поставщиков,
    карточка компании, похожие, таксономия категорий и claim профиля.

    **Не входит в эту схему:** кабинет поставщика, биллинг, admin, webhooks, metrics.

    Для Custom GPT / Gemini / YandexGPT Actions на старте подключайте операции
    тегов `search` и `catalog` (без auth). Операции `claim` требуют Bearer JWT
    после `auth/login` или `auth/register` — неудобны как чистые Actions без OAuth.
  contact:
    name: Найда
    email: info@asknayda.ru
    url: https://asknayda.ru
  license:
    name: Proprietary
    url: https://asknayda.ru/legal/terms

servers:
  - url: https://asknayda.ru/api/v1
    description: Production
  - url: http://192.168.3.10:48087
    description: Staging (u10 LAN)
  - url: http://localhost:8087
    description: Local development

tags:
  - name: search
    description: Поиск поставщиков (BM25 + business rules)
  - name: catalog
    description: Карточка, похожие, таксономия, публичные настройки
  - name: claim
    description: Забрать карточку компании (ownership request)
  - name: auth
    description: Минимальный auth для claim (JWT)
  - name: health
    description: Проверка доступности API

paths:
  /healthz:
    get:
      operationId: getHealthz
      tags: [health]
      summary: Liveness
      description: Процесс жив. Не зависит от Postgres/OpenSearch.
      x-openai-isConsequential: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  /search:
    get:
      operationId: searchSuppliers
      tags: [search]
      summary: Поиск поставщиков
      description: |
        Основной B2B-поиск. Запрос на русском (язык закупки): например
        «производитель ПЛК», «НКУ Чебоксары», «деревообработка».

        Гео из текста («нижний тагил») может быть извлечено в hard-фильтр city.
        Пагинация: предпочтительно `cursor` из `next_cursor`; `from` — shallow offset.
      x-openai-isConsequential: false
      parameters:
        - name: q
          in: query
          required: false
          description: Текст запроса (sourcing language)
          schema:
            type: string
            example: производитель ПЛК
        - name: role
          in: query
          required: false
          description: Роль поставщика
          schema:
            $ref: "#/components/schemas/SupplierRole"
        - name: category
          in: query
          required: false
          description: ID категории таксономии (например plc, build.materials)
          schema:
            type: string
            example: plc
        - name: region
          in: query
          required: false
          description: Код региона РФ (2 цифры)
          schema:
            type: string
            example: "66"
        - name: city
          in: query
          required: false
          description: Город (точное совпадение с индексом)
          schema:
            type: string
            example: Нижний Тагил
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          required: false
          description: Opaque cursor из previous `next_cursor`
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Shallow offset (игнорируется при cursor); max ~9800
          schema:
            type: integer
            minimum: 0
      responses:
        "200":
          description: Результаты поиска
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "504":
          description: Search timeout
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/Unavailable"

  /suppliers/{id}:
    get:
      operationId: getSupplier
      tags: [catalog]
      summary: Карточка поставщика
      description: |
        Публичный профиль. `{id}` — UUID или ИНН (10/12 цифр).
        Контакты маскируются, если нет JWT и карточка не Premium (`contacts_locked=true`).
      x-openai-isConsequential: false
      parameters:
        - $ref: "#/components/parameters/SupplierID"
      responses:
        "200":
          description: Профиль
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupplierProfile"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"

  /suppliers/{id}/similar:
    get:
      operationId: getSimilarSuppliers
      tags: [catalog]
      summary: Похожие поставщики
      description: Топикальный поиск по той же пайплайн-логике, что и `/search`, без самой карточки.
      x-openai-isConsequential: false
      parameters:
        - $ref: "#/components/parameters/SupplierID"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
      responses:
        "200":
          description: Похожие
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResult"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"

  /categories:
    get:
      operationId: listCategories
      tags: [catalog]
      summary: Таксономия категорий
      description: Активные узлы sourcing-таксономии (не ОКВЭД).
      x-openai-isConsequential: false
      responses:
        "200":
          description: Список категорий
          content:
            application/json:
              schema:
                type: object
                required: [items, count]
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/TaxonomyNode"
                  count:
                    type: integer
        "503":
          $ref: "#/components/responses/Unavailable"

  /settings/public:
    get:
      operationId: getPublicSettings
      tags: [catalog]
      summary: Публичные настройки платформы
      x-openai-isConsequential: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  billing_enabled:
                    type: boolean

  /suppliers/{id}/claim-options:
    get:
      operationId: getClaimOptions
      tags: [claim]
      summary: Варианты claim карточки
      description: |
        Возвращает доступные методы (`card_email` / `manual`) и маскированные
        email с карточки. Auth не требуется.
      x-openai-isConsequential: false
      parameters:
        - $ref: "#/components/parameters/SupplierID"
      responses:
        "200":
          description: Опции claim
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClaimOptions"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"

  /suppliers/{id}/claims:
    post:
      operationId: createClaim
      tags: [claim]
      summary: Подать заявку на claim
      description: |
        Требует Bearer JWT и verified email аккаунта.
        Для `card_email` укажите `email_key` из claim-options.
      x-openai-isConsequential: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SupplierID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ClaimRequest"
      responses:
        "201":
          description: Заявка создана
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClaimCreateResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Email не подтверждён
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Claim уже существует
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/Unavailable"

  /claims/verify:
    get:
      operationId: verifyClaim
      tags: [claim]
      summary: Подтвердить claim по magic-link токену
      description: Вызывается из письма; токен одноразовый.
      x-openai-isConsequential: true
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Claim подтверждён (тело зависит от реализации; обычно redirect/JSON ok)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/BadRequest"

  /auth/register:
    post:
      operationId: register
      tags: [auth]
      summary: Регистрация покупателя / заявителя claim
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AuthCredentials"
      responses:
        "201":
          description: Аккаунт создан, JWT выдан
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "409":
          description: Email уже зарегистрирован
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/Unavailable"

  /auth/login:
    post:
      operationId: login
      tags: [auth]
      summary: Вход, получение JWT
      x-openai-isConsequential: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AuthCredentials"
      responses:
        "200":
          description: JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/Unavailable"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "JWT из /auth/login или /auth/register. Header Authorization: Bearer <token>."

  parameters:
    SupplierID:
      name: id
      in: path
      required: true
      description: UUID поставщика или ИНН (10/12 цифр)
      schema:
        type: string
        example: "7707083893"

  responses:
    BadRequest:
      description: Некорректный запрос
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Требуется авторизация
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Не найдено
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unavailable:
      description: Зависимость недоступна
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
        message:
          type: string

    SupplierRole:
      type: string
      enum: [manufacturer, integrator, distributor, service, unknown]

    SearchHit:
      type: object
      required: [id, name_short, role, score, claimed, premium]
      properties:
        id:
          type: string
          format: uuid
        inn:
          type: string
        name_short:
          type: string
        name_full:
          type: string
        brand:
          type: string
        description:
          type: string
        role:
          $ref: "#/components/schemas/SupplierRole"
        region_code:
          type: string
        region:
          type: string
        city:
          type: string
        website:
          type: string
        categories:
          type: array
          items:
            type: string
        products_text:
          type: string
        score:
          type: number
        claimed:
          type: boolean
        premium:
          type: boolean

    FacetBucket:
      type: object
      properties:
        value:
          type: string
        count:
          type: integer
          format: int64

    SearchResult:
      type: object
      required: [items, total]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/SearchHit"
        total:
          type: integer
          format: int64
        next_cursor:
          type: string
          description: Передайте как query `cursor` для следующей страницы
        facets:
          type: object
          additionalProperties:
            type: array
            items:
              $ref: "#/components/schemas/FacetBucket"

    SupplierCategory:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        confidence:
          type: number
        source:
          type: string

    SupplierProduct:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        kind:
          type: string
          enum: [product, service]
        image_key:
          type: string
        image_url:
          type: string
        source:
          type: string
        sort:
          type: integer

    SupplierClassifier:
      type: object
      properties:
        kind:
          type: string
          enum: [okved, okpd2, tnved, other]
        code:
          type: string
        name:
          type: string
        is_main:
          type: boolean
        source:
          type: string

    SupplierContact:
      type: object
      properties:
        type:
          type: string
          description: "email, phone, or other contact channel"
        value:
          type: string
          description: Может быть замаскирован при contacts_locked=true

    ProvenanceFact:
      type: object
      properties:
        field:
          type: string
        value:
          type: string
        source:
          type: string
        collected_at:
          type: string
          format: date-time

    SupplierProfile:
      type: object
      required: [id, name_short, status, role, claimed, premium, contacts_locked]
      properties:
        id:
          type: string
          format: uuid
        inn:
          type: string
        ogrn:
          type: string
        name_short:
          type: string
        name_full:
          type: string
        brand:
          type: string
        status:
          type: string
          enum: [active, liquidating, liquidated, draft]
        role:
          $ref: "#/components/schemas/SupplierRole"
        region_code:
          type: string
        region:
          type: string
        city:
          type: string
        address:
          type: string
        website:
          type: string
        description:
          type: string
        claimed:
          type: boolean
        premium:
          type: boolean
        contacts_locked:
          type: boolean
        profile_view_count:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        categories:
          type: array
          items:
            $ref: "#/components/schemas/SupplierCategory"
        products:
          type: array
          items:
            $ref: "#/components/schemas/SupplierProduct"
        classifiers:
          type: array
          items:
            $ref: "#/components/schemas/SupplierClassifier"
        contacts:
          type: array
          items:
            $ref: "#/components/schemas/SupplierContact"
        provenance:
          type: array
          items:
            $ref: "#/components/schemas/ProvenanceFact"

    TaxonomyNode:
      type: object
      required: [id, name, status, sort]
      properties:
        id:
          type: string
        parent_id:
          type: string
        name:
          type: string
        synonyms:
          type: array
          items:
            type: string
        okved_hints:
          type: array
          items:
            type: string
        status:
          type: string
        sort:
          type: integer

    ClaimEmailOption:
      type: object
      properties:
        key:
          type: string
          description: Opaque key для POST claim (method=card_email)
        masked:
          type: string
          example: i***@example.ru

    ClaimOptions:
      type: object
      properties:
        supplier_id:
          type: string
        supplier_name:
          type: string
        supplier_inn:
          type: string
        method:
          type: string
          enum: [card_email, manual]
        allow_manual:
          type: boolean
        emails:
          type: array
          items:
            $ref: "#/components/schemas/ClaimEmailOption"
        hint:
          type: string

    ClaimRequest:
      type: object
      properties:
        method:
          type: string
          enum: [card_email, manual]
          description: Опционально; сервер может вывести из профиля
        email_key:
          type: string
          description: Обязателен для card_email — из claim-options.emails[].key
        contact_name:
          type: string
        contact_phone:
          type: string
        job_title:
          type: string
        message:
          type: string

    Claim:
      type: object
      properties:
        id:
          type: string
          format: uuid
        supplier_id:
          type: string
        user_id:
          type: string
        applicant_user_id:
          type: string
        contact_name:
          type: string
        contact_email:
          type: string
        contact_phone:
          type: string
        job_title:
          type: string
        message:
          type: string
        status:
          type: string
          enum: [pending, verified, rejected]
        reject_reason:
          type: string
        created_at:
          type: string
          format: date-time
        verified_at:
          type: string
          format: date-time

    ClaimCreateResponse:
      type: object
      properties:
        claim:
          $ref: "#/components/schemas/Claim"
        method:
          type: string
          enum: [card_email, manual]
        mail_sent:
          type: boolean
        needs_manual_review:
          type: boolean
        supplier_name:
          type: string
        supplier_inn:
          type: string
        verify_token:
          type: string
          description: Только в dev при MAIL_EXPOSE_LINK
        verify_url:
          type: string

    AuthCredentials:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8

    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
        email_verified:
          type: boolean
        role:
          type: string
          enum: [user, admin]
        created_at:
          type: string
          format: date-time

    AuthResponse:
      type: object
      required: [token, user]
      properties:
        token:
          type: string
          description: JWT для Authorization Bearer
        user:
          $ref: "#/components/schemas/User"
        mail_sent:
          type: boolean
        verify_url:
          type: string
        verify_token:
          type: string
