openapi: 3.0.3
info:
  title: Мои Места — Public API
  version: 2.1.0
  description: |
    Два независимых API платформы Мои Места в одной спеке:

    1. **Registration API** — публичный API самостоятельной регистрации компании.
       Все эндпоинты тега «Регистрация» и «Аутентификация» открыты (без авторизации).

    2. **Company Management API** (/api/v1/*) — REST API управления подарками и акциями
       для AI-агентов и интеграций. Требует API-ключа компании (Bearer mm_…).

    ВАЖНО ПРО АДРЕСА: Эта спека опубликована на лендинге по адресу
    https://moimesta.ru/api/openapi.yaml, однако сам API живёт на
    https://company.moimesta.ru/api. Используйте адрес из поля servers
    (https://company.moimesta.ru/api), а не адрес, по которому получена спека.
    Пути в этой спеке указаны относительно servers[0].url — полный адрес
    эндпоинта получается склейкой servers[0].url и пути (например, /v1/me
    даёт https://company.moimesta.ru/api/v1/me).

    КРИТИЧНЫЙ QUIRK ОБРАБОТКИ ОШИБОК: Бизнес-ошибки эндпоинтов
    /auth/singup, /auth/singupGetCode и /auth/checkPhone возвращаются с
    HTTP 200 и полем errorMessage в теле ответа — анализируйте тело, а не
    только HTTP-статус. Коды 400 означают только ошибку схемы (валидация JSON),
    429 — превышение rate-limit, 500 — сбой сервера.
    Эндпоинты /auth/restore/* отдают ошибки через HTTP 400 с детализацией
    в поле data.

servers:
  - url: https://company.moimesta.ru/api
    description: Продакшн

# Регистрационные эндпоинты открыты. /api/v1/* требуют ApiKeyAuth.
security: []

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT access-токен, полученный после регистрации или входа.
        Требуется только для защищённых эндпоинтов после регистрации.
        Все регистрационные шаги выполняются без авторизации.

    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: |
        API-ключ компании из личного кабинета (ЛКБ) на company.moimesta.ru.
        Формат: mm_ + 32 hex-символа (пример: mm_0123456789abcdef0123456789abcdef).
        Передаётся заголовком: Authorization: Bearer mm_…
        Ключ привязан к конкретной компании; companyId выводится из ключа
        автоматически — передавать его в теле или URL не нужно.
        Каждый ключ имеет набор scope (gifts:read, gifts:write, promotions:read,
        promotions:write, stats:read). Эндпоинт GET /api/v1/me возвращает
        текущие scopes без авторизационных данных бизнеса.

        УСЛОВИЕ РАБОТЫ КЛЮЧА: API-ключ действует только пока у компании активен
        пробный период ИЛИ оплаченная подписка. Если оба неактивны, любой вызов
        /api/v1/* возвращает HTTP 402 (Payment Required) с телом
        { error, message, entitlement }. После активации пробного периода или
        оплаты подписки ключ снова начинает работать в течение ~30 секунд —
        отзывать или пересоздавать ключ не нужно.

  schemas:
    Category:
      type: object
      required:
        - categoryId
        - title
        - iconUrl
      properties:
        categoryId:
          type: integer
          example: 1
        title:
          type: string
          example: Кафе и рестораны
        iconUrl:
          type: string
          example: https://s3.moimesta.ru/moimesta-static/icons/cafe.svg

    CategoryWithSubcategories:
      allOf:
        - $ref: '#/components/schemas/Category'
        - type: object
          properties:
            subcategories:
              type: array
              items:
                $ref: '#/components/schemas/Category'

    CheckPhoneResponse:
      type: object
      required:
        - userExist
        - companyExist
      properties:
        userExist:
          type: boolean
          example: false
        companyExist:
          type: boolean
          example: false
        errorMessage:
          type: string
          description: |
            Присутствует при бизнес-ошибке (HTTP 200). Анализируйте тело ответа,
            а не только HTTP-статус.

    SendOtpRequest:
      type: object
      required:
        - phone
      properties:
        phone:
          type: number
          description: РФ-номер из 10 цифр без +7
          example: 9991234567

    SendOtpResponse:
      type: object
      required:
        - oneTimePasswordSent
        - retryAfter
      properties:
        oneTimePasswordSent:
          oneOf:
            - type: string
              enum:
                - sms
                - call
                - telegram
                - max
            - type: boolean
              enum:
                - false
          description: |
            Канал доставки OTP или false при бизнес-ошибке.
        smsCodeNumber:
          type: integer
          description: Номер SMS-сообщения (не сам код), может отсутствовать
          example: 42
        retryAfter:
          type: integer
          description: Секунды до следующей попытки
          example: 180
        errorMessage:
          type: string
          description: |
            Присутствует при бизнес-ошибке. Возможные значения:
            "time limit" — повтор слишком рано, ждать retryAfter;
            "company Exist" — компания уже зарегистрирована на этом номере;
            "unknown error" — сбой отправки.
          example: time limit

    SignupRequest:
      type: object
      required:
        - phone
        - email
        - confirmCode
        - companyName
        - categoryId
        - password
      properties:
        phone:
          type: number
          description: РФ-номер из 10 цифр без +7
          example: 9991234567
        email:
          type: string
          format: email
          example: owner@example.ru
        confirmCode:
          type: string
          pattern: '^\d{4}$'
          description: 4-значный OTP-код из шага singupGetCode
          example: '4821'
        companyName:
          type: string
          description: Название компании, не пустое после trim
          example: Кофейня «Утро»
        categoryId:
          type: integer
          description: ID категории из GET /categories
          example: 1
        password:
          type: string
          description: 6–50 символов, обязателен для нового пользователя
          example: MyPass123
        username:
          type: string
          description: Имя, 3–30 символов, обязателен для нового пользователя (ветка A)
          example: Иван
        userSurname:
          type: string
          description: Фамилия, 3–30 символов, обязателен для нового пользователя (ветка A)
          example: Петров

    SignupUser:
      type: object
      required:
        - userId
        - userName
        - companyId
        - title
        - balance
        - status
      properties:
        userId:
          type: integer
          example: 123456
        userName:
          type: string
          example: Иван
        companyId:
          type: integer
          example: 78910
        title:
          type: string
          example: Кофейня «Утро»
        balance:
          type: number
          example: 0
        status:
          type: object
          properties:
            activated:
              type: boolean
              example: true
            isTrialPeriodActive:
              type: boolean
              example: true

    SignupResponse:
      type: object
      properties:
        accessToken:
          type: string
          description: JWT access-токен (при успехе)
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        refreshToken:
          type: string
          description: JWT refresh-токен (при успехе)
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        user:
          $ref: '#/components/schemas/SignupUser'
        errorMessage:
          type: string
          description: |
            Присутствует при бизнес-ошибке (HTTP 200). Возможные значения:
            "not match" — неверный код;
            "code expired" — код старше 3 минут, запросить новый;
            "error" — нет отправленного кода для номера;
            "company Exist" — компания уже есть;
            "user data required" — не переданы password/username/userSurname;
            "invalid company data" — пустой companyName или несуществующий categoryId.

    RestoreOtpRequest:
      type: object
      required:
        - phone
      properties:
        phone:
          type: number
          description: РФ-номер из 10 цифр без +7
          example: 9991234567

    RestoreOtpResponse:
      type: object
      properties:
        oneTimePasswordSent:
          type: string
          enum:
            - sms
            - call
            - telegram
          example: sms
        retryAfter:
          type: integer
          example: 180
        smsCodeNumber:
          type: integer
          example: 42

    RestoreChangePasswordRequest:
      type: object
      required:
        - phone
        - oneTimePassword
        - password
        - confirmPassword
      properties:
        phone:
          type: number
          description: РФ-номер из 10 цифр без +7
          example: 9991234567
        oneTimePassword:
          type: string
          pattern: '^\d{4}$'
          description: 4-значный OTP из шага restore/one-time-password
          example: '4821'
        password:
          type: string
          description: Новый пароль, 6–50 символов
          example: NewPass123
        confirmPassword:
          type: string
          description: Должен совпадать с password
          example: NewPass123

    AuthLoginRequest:
      type: object
      required:
        - username
        - password
      properties:
        username:
          type: string
          description: Телефон в виде строки (10 цифр)
          example: '9991234567'
        password:
          type: string
          example: MyPass123

    AuthTokensResponse:
      type: object
      properties:
        accessToken:
          type: string
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        refreshToken:
          type: string
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        user:
          $ref: '#/components/schemas/SignupUser'

    AccessTokenRequest:
      type: object
      required:
        - accessToken
      properties:
        accessToken:
          type: string
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

    AccessTokenResponse:
      type: object
      properties:
        accessToken:
          type: string
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        user:
          $ref: '#/components/schemas/SignupUser'

    RefreshTokenRequest:
      type: object
      required:
        - refreshToken
      properties:
        refreshToken:
          type: string
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

    ErrorResponse:
      type: object
      properties:
        status:
          type: string
          example: error
        message:
          type: string

    RateLimitError:
      type: object
      properties:
        message:
          type: string
          example: Too many OTP requests for this phone

    RestoreError:
      type: object
      properties:
        status:
          type: string
          example: error
        message:
          type: string
        data:
          type: object
          description: |
            Содержит ключ с причиной ошибки:
            cannotBeSended — нет компании на номере или слишком частый запрос;
            invalidOneTimePassword — код не совпал;
            invalidUser — нет прав или компании;
            invalidPassword — password не совпадает с confirmPassword.

    # ── /api/v1 — Company Management API ────────────────────────────────────

    ApiKeyMeResponse:
      type: object
      required:
        - companyId
        - scopes
      properties:
        companyId:
          type: integer
          description: ID компании, к которой привязан ключ
          example: 78910
        companyName:
          type: string
          nullable: true
          description: Название компании (null если не удалось получить)
          example: Кофейня «Утро»
        scopes:
          type: array
          items:
            type: string
            enum:
              - gifts:read
              - gifts:write
              - promotions:read
              - promotions:write
              - stats:read
          description: Список scope, доступных этому ключу
          example:
            - gifts:read
            - promotions:read

    GiftType:
      type: string
      enum:
        - certificate
        - discountCoupon
        - bonusesCoupon
        - bonusReward
      description: |
        Тип подарка. Значения camelCase — snake_case (discount_coupon и т.п.) не
        распознаётся валидацией и вернёт HTTP 400 validation_error:
        - certificate — Сертификат (valuePrice — номинал в рублях)
        - discountCoupon — Скидка / купон на скидку (discountPercent — процент скидки)
        - bonusesCoupon — Бонусы / бонусный купон (bonusesAmount — кол-во монет, bonusesPercent — процент)
        - bonusReward — Подарочные бонусы / бонусное вознаграждение (bonusesAmount — монеты, rewardTTL — дни жизни монет)

    GiftBase:
      type: object
      required:
        - giftId
        - title
        - type
        - startedAt
        - expiredAt
      properties:
        id:
          type: integer
          description: >-
            Канонический идентификатор для путей GET/обновления /{id}
            (для подарков совпадает с giftId). Правило: какой `id` пришёл в
            ответе — такой и подставляйте в путь. Не используйте promotionId.
          example: 1001
        giftId:
          type: integer
          example: 1001
        title:
          type: string
          description: Название подарка
          example: Скидка 10% на кофе
        type:
          $ref: '#/components/schemas/GiftType'
        description:
          type: string
          description: Описание подарка (может содержать HTML из разметки)
          example: Подарите клиенту скидку 10% на любой кофе
        condition:
          type: string
          description: Условие получения подарка
          example: При покупке от 500 рублей
        pictureUrls:
          type: array
          items:
            type: string
            format: uri
          description: Ссылки на изображения подарка
        startedAt:
          type: integer
          format: int64
          description: Дата начала действия (unix timestamp, мс)
          example: 1720000000000
        expiredAt:
          type: integer
          format: int64
          description: Дата окончания действия (unix timestamp, мс)
          example: 1730000000000
        valuePrice:
          type: number
          description: Номинал сертификата в рублях (только для type=certificate)
          example: 500
        discountPercent:
          type: number
          description: Процент скидки (только для type=discountCoupon)
          example: 10
        bonusesAmount:
          type: integer
          description: Количество монет (только для type=bonusesCoupon и bonusReward)
          example: 50
        bonusesPercent:
          type: number
          description: Процент бонусов (только для type=bonusesCoupon)
          example: 5
        rewardTTL:
          type: integer
          description: Время жизни монет в днях (только для type=bonusReward)
          example: 30
        hide:
          type: boolean
          description: Скрыт ли подарок
          example: false
        moderation:
          type: string
          nullable: true
          description: Статус модерации (pending/approved/rejected/null)
          example: approved

    GiftCreateRequest:
      type: object
      required:
        - title
        - type
        - startedAt
        - expiredAt
      properties:
        title:
          type: string
          description: Название подарка (не пустое)
          example: Скидка 10% на кофе
        type:
          $ref: '#/components/schemas/GiftType'
        description:
          type: string
          description: Описание (HTML разрешён)
          example: Подарите клиенту скидку 10% на любой кофе
        condition:
          type: string
          description: Условие получения
          example: При покупке от 500 рублей
        startedAt:
          type: integer
          format: int64
          description: Начало действия (unix timestamp, мс)
          example: 1720000000000
        expiredAt:
          type: integer
          format: int64
          description: Конец действия (unix timestamp, мс)
          example: 1730000000000
        valuePrice:
          type: number
          description: Номинал сертификата (обязателен для type=certificate)
          example: 500
        discountPercent:
          type: number
          description: Процент скидки (обязателен для type=discountCoupon)
          example: 10
        bonusesAmount:
          type: integer
          description: Кол-во монет (обязателен для type=bonusesCoupon, bonusReward)
          example: 50
        bonusesPercent:
          type: number
          description: Процент бонусов (для type=bonusesCoupon)
          example: 5
        rewardTTL:
          type: integer
          description: Время жизни монет в днях (для type=bonusReward)
          example: 30
        pictures:
          type: array
          maxItems: 3
          description: |
            Изображения подарка — массив строк, до 3 штук.
            Каждый элемент — либо base64 data-URI (data:image/jpeg;base64,… или
            data:image/png;base64,…), либо уже существующий URL изображения.
            Первый элемент становится обложкой подарка.
            Картинки передаются инлайном; сервер сохраняет изображение самостоятельно.
            Рекомендуемое соотношение сторон: 16:9. Максимальный размер: 2 МБ на фото.
          items:
            type: string
          example:
            - "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."

    GiftListResponse:
      type: object
      required:
        - items
        - rows
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/GiftBase'
        rows:
          type: integer
          description: Общее кол-во подарков (для пагинации)
          example: 42

    GiftStatsResponse:
      type: object
      properties:
        gift:
          $ref: '#/components/schemas/GiftBase'
        countCompaniesBuyed:
          type: integer
          description: Кол-во компаний, купивших подарок
          example: 5
        countNewClients:
          type: integer
          description: Кол-во новых клиентов
          example: 12
        countNotApplied:
          type: integer
          description: Не применено
          example: 3
        countApplied:
          type: integer
          description: Применено
          example: 20
        countUsed:
          type: integer
          description: Использовано
          example: 18
        countDeclined:
          type: integer
          description: Отклонено
          example: 2
        turnOver:
          type: number
          description: Оборот (рублей)
          example: 9500

    ApiV1Error:
      type: object
      required:
        - error
      description: |
        Стандартный формат ошибки для всех эндпоинтов /api/v1.
        Тело НИКОГДА не содержит внутренних деталей (имён таблиц, стека, SQL).
        Поле error — стабильный машиночитаемый код; поле message — человекочитаемое описание.
        При ошибке валидации присутствует массив fields с детализацией по полям.
      properties:
        error:
          type: string
          description: |
            Стабильный код ошибки (snake_case). Основные коды:
            - invalid_api_key — ключ не найден или отозван (HTTP 401)
            - insufficient_scope — нет нужного scope (HTTP 403)
            - validation_error — некорректные поля запроса (HTTP 400)
            - gift_after_share_time — срок действия подарка не перекрывает период акции + 3 дня (HTTP 400)
            - not_enough_gift_amount — недостаточно доступных подарков (HTTP 400)
            - invalid_promotion_dates — некорректные даты акции (HTTP 400)
            - promocode_not_verified — акция ещё не прошла модерацию (HTTP 400)
            - card_codes_empty — список кодов карт пуст (HTTP 400)
            - card_codes_limit_exceeded — карт больше 1000 (HTTP 400)
            - card_not_found — карта не найдена или не принадлежит компании (HTTP 400)
            - card_not_our_client — клиент не зарегистрирован в компании (HTTP 400)
            - card_user_not_found — карта не привязана к пользователю (HTTP 400)
            - gift_not_found — подарок не найден или недоступен (HTTP 400)
            - gift_type_mismatch — неверный тип подарка (нужен certificate) (HTTP 400)
            - gift_insufficient_amount — запрошено больше, чем доступно (HTTP 400)
            - gift_expiration_exceeded — срок подарка слишком короткий для акции (HTTP 400)
            - promotion_not_found — акция не найдена (HTTP 404)
            - subscription_required — нет ни подписки, ни пробного периода (HTTP 402)
            - trial_expired — пробный период закончился, подписки не было (HTTP 402)
            - subscription_inactive — подписка была, но истекла или не оплачена (HTTP 402)
            - target_config_required — не переданы настройки таргета (HTTP 400)
            - no_target_clients — по параметрам таргета нет клиентов (HTTP 400)
            - recommendation_percent_already_active — уже есть активная процентная программа (HTTP 400)
            - recommendation_gift_not_own — можно использовать только собственный подарок (HTTP 400)
            - recommendation_not_bonus_company — percent-программа только для бонусных компаний (HTTP 400)
            - recommendation_gift_expired — срок подарка недостаточен для программы (HTTP 400)
            - recommendation_not_enough_purchased — недостаточно купленных подарков (HTTP 400)
            - recommendation_cannot_change_type — нельзя изменить тип программы (HTTP 400)
            - recommendation_referrer_gift_not_found — подарок для реферала не найден (HTTP 400)
            - recommendation_gift_not_started — дата начала подарка ещё не наступила (HTTP 400)
            - recommendation_not_found — реферальная программа не найдена (HTTP 404)
            - not_found — запрашиваемый объект не найден (используется, в частности,
              close-роутами one-time-gift/gift-for-coins/smart-ad, когда акция не найдена
              или принадлежит другой компании) (HTTP 404)
            - business_error — операция отклонена бизнес-правилом без более специфичного
              кода (используется, в частности, close-роутами one-time-gift/gift-for-coins/
              smart-ad при попытке остановить уже остановленную или истёкшую акцию) (HTTP 400)
            - too_many_requests — rate-limit превышен (HTTP 429)
            - internal_error — внутренняя ошибка сервиса (HTTP 500)
          example: insufficient_scope
        message:
          type: string
          description: Человекочитаемое описание ошибки
          example: Недостаточно прав для выполнения операции.
        required:
          type: string
          description: Требуемый scope (только для insufficient_scope)
          example: gifts:write
        entitlement:
          type: string
          enum:
            - no_subscription
            - trial_active
            - trial_grace
            - active
            - grace
            - trial_blocked
            - blocked
          description: |
            Состояние доступа компании (присутствует только в ответах HTTP 402).
            Показывает, почему ключ временно не работает:
            - no_subscription — нет ни подписки, ни пробного периода;
            - trial_blocked — пробный период закончился, подписки не было;
            - blocked — подписка была, но истекла или не оплачена.
            Значения trial_active / trial_grace / active / grace соответствуют
            рабочему состоянию ключа и в теле ошибки 402 не встречаются.
          example: trial_blocked
        fields:
          type: array
          description: Список ошибок по полям (только для validation_error)
          items:
            type: object
            properties:
              field:
                type: string
                example: giftId
              message:
                type: string
                example: Обязательное поле отсутствует

    ApiKeyError:
      $ref: '#/components/schemas/ApiV1Error'

    PromotionGiftsItem:
      type: object
      description: Запись «разовый подарок» (one-time-gift promotion)
      properties:
        id:
          type: integer
          description: >-
            Канонический идентификатор для пути GET /promotions/one-time-gift/{id}
            (совпадает с promotionGiftsId). Используйте именно `id` из ответа
            create/list, а не promotionId.
          example: 501
        promotionGiftsId:
          type: integer
          example: 501
        promotionId:
          type: integer
          example: 200
        companyId:
          type: integer
          example: 78910
        title:
          type: string
          example: Ноябрьская акция
        description:
          type: string
          example: Дарим кофе всем новым клиентам
        giftId:
          type: integer
          example: 1001
        giftAmount:
          type: integer
          description: Кол-во подарков для раздачи
          example: 100
        giftExpires:
          type: integer
          description: Время жизни подарка у клиента (дни)
          example: 30
        giftOfferExpires:
          type: integer
          description: Дней до того, как клиент должен открыть подарок
          example: 7
        cardCodes:
          type: string
          description: Коды карт получателей (разделённые переносом строки или запятой)
          example: "1234567890\n0987654321"
        statGiftsSent:
          type: integer
          example: 98
        statGiftsClaimed:
          type: integer
          example: 60
        statGiftsRejected:
          type: integer
          example: 5
        statGiftsExpired:
          type: integer
          example: 10
        statGiftsUsed:
          type: integer
          example: 55
        createdAt:
          type: integer
          format: int64
          example: 1720000000000
        startsAt:
          type: integer
          format: int64
          example: 1720000000000
        expiredAt:
          type: integer
          format: int64
          example: 1730000000000

    PromotionGiftsFindResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          example: success
        data:
          type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/PromotionGiftsItem'
            rows:
              type: integer
              example: 10

    PromotionGiftsGetResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          example: success
        data:
          $ref: '#/components/schemas/PromotionGiftsItem'

    PromotionGiftsCreateRequest:
      type: object
      required:
        - giftId
        - title
        - description
        - giftAmount
        - giftExpires
        - giftOfferExpires
        - cardCodes
      properties:
        giftId:
          type: integer
          description: ID подарка из GET /api/v1/gifts (тип certificate)
          example: 1001
        title:
          type: string
          description: Название акции
          example: Ноябрьская акция
        description:
          type: string
          description: Описание акции
          example: Дарим кофе всем новым клиентам
        giftAmount:
          type: integer
          description: Кол-во подарков для раздачи (≥ числа карт)
          example: 100
        giftExpires:
          type: integer
          description: Время жизни подарка у клиента (дни)
          example: 30
        giftOfferExpires:
          type: integer
          description: Дней до первого открытия оффера
          example: 7
        cardCodes:
          type: string
          description: |
            Коды карт получателей (каждый на новой строке или разделены запятой).
            Лимит: до 1000 карт на акцию.
          example: "1234567890\n0987654321"

    PromotionGiftsCreateResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          example: success
        data:
          $ref: '#/components/schemas/PromotionGiftsItem'

    PromocodeItem:
      type: object
      description: Запись «подарок за покупку» (gift-for-coins / promocode)
      properties:
        id:
          type: integer
          description: >-
            Канонический идентификатор для пути GET /promotions/gift-for-coins/{id}
            (совпадает с promotionPromocodeId). Используйте именно `id` из ответа
            create/list, а не promotionId.
          example: 301
        promotionPromocodeId:
          type: integer
          example: 301
        promotionId:
          type: integer
          example: 200
        title:
          type: string
          example: Кофе за монеты
        description:
          type: string
          example: Обменяй 100 монет на кофе
        giftId:
          type: integer
          example: 1001
        amount:
          type: integer
          description: Кол-во акций (количество промокодов)
          example: 50
        moneySpend:
          type: number
          description: Стоимость в монетах (coins), которую клиент должен потратить
          example: 100
        startedAt:
          type: integer
          format: int64
          example: 1720000000000
        expiredAt:
          type: integer
          format: int64
          example: 1730000000000
        moderation:
          type: string
          nullable: true
          example: approved
        autoActivate:
          type: boolean
          description: Активировать автоматически при выполнении условий
          example: false
        createdAt:
          type: integer
          format: int64
          example: 1720000000000

    PromocodeFindResponse:
      oneOf:
        - type: array
          items:
            $ref: '#/components/schemas/PromocodeItem'
        - $ref: '#/components/schemas/PromocodeItem'
      description: |
        Список (GET /promotions/gift-for-coins) или один объект (GET /promotions/gift-for-coins/{id}).

    PromocodeCreateRequest:
      type: object
      required:
        - title
        - giftId
        - amount
        - startDate
        - endDate
        - moneySpend
        - pictureSmall
        - pictureMiddle
      properties:
        title:
          type: string
          description: Название акции
          example: Кофе за монеты
        description:
          type: string
          description: Описание акции
          example: Обменяй монеты на кофе
        giftId:
          type: integer
          description: ID подарка из GET /api/v1/gifts
          example: 1001
        amount:
          type: integer
          description: Кол-во промокодов для раздачи
          example: 50
        startDate:
          type: string
          pattern: '^\d{4}-\d{2}-\d{2}$'
          description: Дата начала (YYYY-MM-DD)
          example: '2026-09-01'
        endDate:
          type: string
          pattern: '^\d{4}-\d{2}-\d{2}$'
          description: Дата окончания (YYYY-MM-DD)
          example: '2026-11-30'
        moneySpend:
          type: number
          description: Стоимость в монетах
          example: 100
        pictureSmall:
          type: string
          description: |
            Изображение для карточки акции (квадратное, соотношение 1:1).
            Передаётся инлайном как base64 data-URI (data:image/jpeg;base64,… или
            data:image/png;base64,…). Сервер сохраняет изображение самостоятельно.
            Максимальный размер: 2 МБ. Обязательно при создании акции.
          example: "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
        pictureMiddle:
          type: string
          description: |
            Изображение для описания акции (соотношение 16:10).
            Передаётся инлайном как base64 data-URI (data:image/jpeg;base64,… или
            data:image/png;base64,…). Сервер сохраняет изображение самостоятельно.
            Максимальный размер: 2 МБ. Обязательно при создании акции.
          example: "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
        autoActivate:
          type: boolean
          description: Активировать автоматически
          example: false

    PromocodeCreateResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - successful
            - error
            - GiftNotFound
            - NotEnoughGiftAmount
            - PromocodeIsNotVerified
            - GiftAfterShareTime
            - InvalidPromotionActivityDates
          example: successful
        items:
          $ref: '#/components/schemas/PromocodeItem'

    # ── Smart-ad (target promotion, card_code only) ──────────────────────────

    SmartAdItem:
      type: object
      description: Запись «Smart реклама» (smart-ad / target promotion, только card_code)
      properties:
        id:
          type: integer
          example: 401
        promotionId:
          type: integer
          example: 200
        companyId:
          type: integer
          example: 78910
        title:
          type: string
          example: Персональная акция для VIP
        description:
          type: string
          example: Отправляем подарок выбранным картам
        giftId:
          type: integer
          example: 1001
        giftAmount:
          type: integer
          description: Кол-во подарков
          example: 100
        giftExpires:
          type: integer
          description: Время жизни подарка у клиента (дни)
          example: 30
        giftOfferExpires:
          type: integer
          description: Дней до первого открытия оффера
          example: 7
        targetMode:
          type: string
          enum:
            - card_code
          description: |
            Режим адресации. Через API-ключ доступен ТОЛЬКО режим card_code
            (список карт передаётся явно). Режим target_schema доступен только через ЛКБ.
          example: card_code
        cardCodes:
          type: string
          description: Коды карт получателей
          example: "1234567890\n0987654321"
        createdAt:
          type: integer
          format: int64
          example: 1720000000000

    SmartAdCreateRequest:
      type: object
      required:
        - title
        - giftId
        - giftAmount
        - giftExpires
        - giftOfferExpires
        - cardCodes
        - targetMode
      properties:
        title:
          type: string
          description: Название акции
          example: Персональная акция для VIP
        description:
          type: string
          description: Описание акции
          example: Отправляем подарок выбранным картам
        giftId:
          type: integer
          description: |
            ID подарка (тип certificate) из GET /api/v1/gifts.
            Срок действия подарка (expiredAt) должен заканчиваться минимум на 3 дня
            ПОЗЖЕ даты окончания акции — иначе ошибка gift_after_share_time.
          example: 1001
        giftAmount:
          type: integer
          description: Кол-во подарков для раздачи (≥ числа карт)
          example: 100
        giftExpires:
          type: integer
          description: Дней жизни подарка у клиента
          example: 30
        giftOfferExpires:
          type: integer
          description: Дней до первого открытия оффера
          example: 7
        targetMode:
          type: string
          enum:
            - card_code
          description: |
            Через API-ключ поддерживается ТОЛЬКО card_code. Любое другое
            значение вернёт HTTP 400 с error=invalid_target_mode.
          example: card_code
        cardCodes:
          type: string
          description: |
            Коды карт получателей (каждый на новой строке или через запятую).
            Лимит: до 1000 карт.
          example: "1234567890\n0987654321"

    SmartAdFindResponse:
      type: object
      required:
        - items
        - rows
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/SmartAdItem'
        rows:
          type: integer
          example: 5

    # ── Recommendation (referral program) ───────────────────────────────────

    RecommendationPercentLevels:
      type: object
      description: Процент вознаграждения по уровням реферальной цепочки
      properties:
        '1':
          type: number
          description: Процент для уровня 1 (прямой реферал)
          example: 5
        '2':
          type: number
          description: Процент для уровня 2
          example: 3
        '3':
          type: number
          description: Процент для уровня 3
          example: 1

    RecommendationRecipientOrSender:
      type: object
      description: Параметры подарка для получателя или реферера (один раз)
      properties:
        giftId:
          type: integer
          description: |
            ID подарка. Срок действия подарка (expiredAt) должен заканчиваться
            минимум на 3 дня ПОЗЖЕ даты окончания программы (endDate) —
            иначе ошибка gift_after_share_time.
          example: 2001
        giftAmount:
          type: integer
          description: Количество подарков
          example: 1
        expiredDays:
          type: integer
          description: Дней жизни подарка у клиента после получения
          example: 30

    RecommendationCreatePercentRequest:
      type: object
      description: Создание процентной акции «Рекомендации» (реферальная механика, type=percent)
      required:
        - type
        - condition
        - giftId
        - levels
        - startDate
        - endDate
        - pictureSmall
        - pictureMiddle
      properties:
        type:
          type: string
          enum:
            - percent
          description: Тип программы — процент от покупок рефералов
          example: percent
        condition:
          type: string
          description: Текстовое описание условия участия
          example: Приведи друга и получай 5% с каждой его покупки
        giftId:
          type: integer
          description: |
            ID подарка за участие. Только собственный подарок компании.
            Срок действия подарка (expiredAt) должен заканчиваться минимум на 3 дня
            ПОЗЖЕ даты окончания программы (endDate) — иначе ошибка gift_after_share_time.
            Доступно только компаниям с бонусной системой.
          example: 2001
        levels:
          $ref: '#/components/schemas/RecommendationPercentLevels'
        startDate:
          type: string
          pattern: '^\d{4}-\d{2}-\d{2}$'
          description: Дата начала программы (YYYY-MM-DD)
          example: '2026-09-01'
        endDate:
          type: string
          pattern: '^\d{4}-\d{2}-\d{2}$'
          description: Дата окончания программы (YYYY-MM-DD). Срок подарка должен заканчиваться минимум на 3 дня позже.
          example: '2027-09-01'
        pictureSmall:
          type: string
          description: |
            Изображение карточки программы (1:1, квадрат).
            Base64 data-URI (data:image/jpeg;base64,… или data:image/png;base64,…).
            Максимум 2 МБ.
          example: "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
        pictureMiddle:
          type: string
          description: |
            Изображение описания программы (16:10).
            Base64 data-URI (data:image/jpeg;base64,… или data:image/png;base64,…).
            Максимум 2 МБ.
          example: "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."

    RecommendationCreateOnceGiftRequest:
      type: object
      description: Создание программы «единовременный подарок» (type=once-gift)
      required:
        - type
        - condition
        - startDate
        - endDate
        - pictureSmall
        - pictureMiddle
        - recipient
        - sender
      properties:
        type:
          type: string
          enum:
            - once-gift
          description: Тип программы — единовременный подарок при выполнении условия
          example: once-gift
        condition:
          type: string
          description: Текстовое описание условия участия
          example: При первой покупке нового клиента оба получают подарок
        startDate:
          type: string
          pattern: '^\d{4}-\d{2}-\d{2}$'
          description: Дата начала программы (YYYY-MM-DD)
          example: '2026-09-01'
        endDate:
          type: string
          pattern: '^\d{4}-\d{2}-\d{2}$'
          description: Дата окончания программы (YYYY-MM-DD). Срок подарков (recipient и sender) должен заканчиваться минимум на 3 дня позже.
          example: '2027-09-01'
        recipientMinBuySum:
          type: number
          description: Минимальная сумма покупки нового клиента для активации программы
          example: 500
        recipient:
          $ref: '#/components/schemas/RecommendationRecipientOrSender'
        sender:
          $ref: '#/components/schemas/RecommendationRecipientOrSender'
        pictureSmall:
          type: string
          description: |
            Изображение карточки программы (1:1, квадрат).
            Base64 data-URI. Максимум 2 МБ.
          example: "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
        pictureMiddle:
          type: string
          description: |
            Изображение описания программы (16:10).
            Base64 data-URI. Максимум 2 МБ.
          example: "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."

    RecommendationItem:
      type: object
      description: Запись акции «Рекомендации» (реферальная механика)
      properties:
        id:
          type: integer
          description: >-
            Канонический идентификатор для путей GET/обновления/закрытия
            /promotions/recommendation/{id} (совпадает с recommendationId).
            Используйте именно `id` из ответа create/list, а не promotionId.
          example: 601
        recommendationId:
          type: integer
          example: 601
        companyId:
          type: integer
          example: 78910
        type:
          type: string
          enum:
            - percent
            - once-gift
          example: percent
        condition:
          type: string
          example: Приведи друга и получай бонусы
        status:
          type: string
          description: Статус программы (active, stopped, expired и т.п.)
          example: active
        startDate:
          type: string
          example: '2026-09-01'
        endDate:
          type: string
          example: '2027-09-01'
        participantsCount:
          type: integer
          description: Количество участников программы
          example: 42
        createdAt:
          type: integer
          format: int64
          example: 1720000000000
        startedAt:
          type: integer
          format: int64
          nullable: true
          example: 1720000000000
        stoppedAt:
          type: integer
          format: int64
          nullable: true
          example: null

    RecommendationFindResponse:
      type: object
      required:
        - items
        - rows
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/RecommendationItem'
        rows:
          type: integer
          example: 5

    RecommendationActiveResponse:
      type: object
      required:
        - active
      properties:
        active:
          type: boolean
          description: |
            true — у компании есть активная процентная (type=percent) акция «Рекомендации» (реферальная механика).
            Максимум 1 активная percent-программа на компанию.
          example: false

paths:
  /categories:
    get:
      summary: Справочник категорий
      operationId: getCategories
      description: Возвращает список бизнес-категорий для регистрации компании. Идемпотентен.
      tags:
        - Регистрация
      responses:
        '200':
          description: Список категорий
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Category'
              examples:
                success:
                  summary: Успешный ответ
                  value:
                    - categoryId: 1
                      title: Кафе и рестораны
                      iconUrl: https://company.moimesta.ru/api/categories/1/icon.svg
                    - categoryId: 2
                      title: Красота и здоровье
                      iconUrl: https://company.moimesta.ru/api/categories/2/icon.svg
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /categories/findWithSubcatigories:
    get:
      summary: Категории с подкатегориями
      operationId: getCategoriesWithSubcategories
      description: Возвращает категории вместе с вложенными подкатегориями.
      tags:
        - Регистрация
      responses:
        '200':
          description: Список категорий с подкатегориями
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CategoryWithSubcategories'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/checkPhone:
    get:
      summary: Проверка телефона
      operationId: checkPhone
      description: |
        Определяет ветку сценария регистрации по номеру телефона.
        При companyExist=true регистрацию следует прервать и перейти к логину
        или восстановлению пароля.

        БИЗНЕС-ОШИБКИ возвращаются с HTTP 200 и полем errorMessage — проверяйте тело.
      tags:
        - Регистрация
      parameters:
        - name: phone
          in: query
          required: true
          description: РФ-номер из 10 цифр без +7
          schema:
            type: integer
            example: 9991234567
      responses:
        '200':
          description: Результат проверки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckPhoneResponse'
              examples:
                newUser:
                  summary: Новый пользователь и компания (ветка A)
                  value:
                    userExist: false
                    companyExist: false
                existingUser:
                  summary: Физлицо без компании (ветка B)
                  value:
                    userExist: true
                    companyExist: false
                existingCompany:
                  summary: Компания уже есть (ветка C — прервать регистрацию)
                  value:
                    userExist: true
                    companyExist: true
        '400':
          description: Некорректный phone (ошибка схемы)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/singupGetCode:
    post:
      summary: Запрос OTP-кода
      operationId: singupGetCode
      description: |
        Отправляет одноразовый код (OTP) на телефон через SMS, звонок или Telegram.

        Rate-limit: 3 попытки / 10 мин на номер, 20 / 10 мин на IP, 100 / мин глобально.

        БИЗНЕС-ОШИБКИ возвращаются с HTTP 200 и полем errorMessage — проверяйте тело.
      tags:
        - Регистрация
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendOtpRequest'
            examples:
              request:
                summary: Запрос OTP
                value:
                  phone: 9991234567
      responses:
        '200':
          description: OTP отправлен или бизнес-ошибка (проверяйте errorMessage)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendOtpResponse'
              examples:
                success:
                  summary: OTP отправлен
                  value:
                    oneTimePasswordSent: sms
                    smsCodeNumber: 42
                    retryAfter: 180
                timeLimit:
                  summary: Бизнес-ошибка — повтор слишком рано
                  value:
                    oneTimePasswordSent: false
                    retryAfter: 240
                    errorMessage: time limit
                companyExists:
                  summary: Бизнес-ошибка — компания уже есть
                  value:
                    oneTimePasswordSent: false
                    retryAfter: 0
                    errorMessage: company Exist
        '400':
          description: Ошибка схемы запроса
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Rate-limit превышен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/singup:
    post:
      summary: Регистрация компании
      operationId: singup
      description: |
        Верифицирует OTP-код и создаёт компанию. При успехе возвращает токены и данные пользователя.

        Ветка A (новый пользователь): поля username, userSurname, password обязательны.
        Ветка B (существующее физлицо): username и userSurname можно опустить.

        БИЗНЕС-ОШИБКИ возвращаются с HTTP 200 и полем errorMessage — проверяйте тело.
      tags:
        - Регистрация
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignupRequest'
            examples:
              branchA:
                summary: Ветка A — новый пользователь
                value:
                  phone: 9991234567
                  username: Иван
                  userSurname: Петров
                  password: MyPass123
                  email: owner@example.ru
                  confirmCode: '4821'
                  companyName: Кофейня «Утро»
                  categoryId: 1
              branchB:
                summary: Ветка B — существующее физлицо
                value:
                  phone: 9991234567
                  password: MyPass123
                  email: owner@example.ru
                  confirmCode: '4821'
                  companyName: Кофейня «Утро»
                  categoryId: 1
      responses:
        '200':
          description: Успех или бизнес-ошибка (проверяйте errorMessage)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignupResponse'
              examples:
                success:
                  summary: Компания создана
                  value:
                    accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
                    refreshToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
                    user:
                      userId: 123456
                      userName: Иван
                      companyId: 78910
                      title: Кофейня «Утро»
                      balance: 0
                      status:
                        activated: true
                        isTrialPeriodActive: true
                notMatch:
                  summary: Бизнес-ошибка — неверный код
                  value:
                    errorMessage: not match
                codeExpired:
                  summary: Бизнес-ошибка — код истёк
                  value:
                    errorMessage: code expired
                userDataRequired:
                  summary: Бизнес-ошибка — не переданы данные пользователя
                  value:
                    errorMessage: user data required
                invalidCompanyData:
                  summary: Бизнес-ошибка — некорректные данные компании
                  value:
                    errorMessage: invalid company data
        '400':
          description: Ошибка схемы запроса
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/restore/one-time-password:
    post:
      summary: Запрос кода восстановления пароля
      operationId: restoreOneTimePassword
      description: |
        Отправляет OTP для восстановления пароля. Используется, если checkPhone
        вернул companyExist=true.

        Rate-limit: не чаще 1 раза / 1 мин на номер.
        Ошибки возвращаются через HTTP 400 с деталями в поле data.
      tags:
        - Восстановление пароля
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RestoreOtpRequest'
            examples:
              request:
                summary: Запрос кода восстановления
                value:
                  phone: 9991234567
      responses:
        '200':
          description: OTP для восстановления отправлен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RestoreOtpResponse'
              examples:
                success:
                  summary: Код отправлен
                  value:
                    oneTimePasswordSent: sms
                    retryAfter: 180
                    smsCodeNumber: 43
        '400':
          description: Ошибка (нет компании на номере, слишком частый запрос)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RestoreError'
              examples:
                cannotBeSended:
                  summary: Нет компании или слишком часто
                  value:
                    status: error
                    message: ''
                    data:
                      cannotBeSended: {}
        '429':
          description: Rate-limit превышен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/restore/change-password:
    post:
      summary: Смена пароля по OTP
      operationId: restoreChangePassword
      description: |
        Устанавливает новый пароль по OTP-коду из шага restore/one-time-password.
        Ошибки возвращаются через HTTP 400 с деталями в поле data.
      tags:
        - Восстановление пароля
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RestoreChangePasswordRequest'
            examples:
              request:
                summary: Смена пароля
                value:
                  phone: 9991234567
                  oneTimePassword: '4821'
                  password: NewPass123
                  confirmPassword: NewPass123
      responses:
        '200':
          description: Пароль успешно изменён (тело пустое)
          content:
            application/json:
              schema:
                type: object
        '400':
          description: Ошибка (неверный код, нет прав, пароли не совпадают)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RestoreError'
              examples:
                invalidCode:
                  summary: Неверный OTP
                  value:
                    status: error
                    message: ''
                    data:
                      invalidOneTimePassword: {}
                invalidPassword:
                  summary: Пароли не совпадают
                  value:
                    status: error
                    message: ''
                    data:
                      invalidPassword: {}
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth:
    post:
      summary: Вход по паролю
      operationId: login
      description: Вход в систему по номеру телефона (как строка) и паролю.
      tags:
        - Аутентификация
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthLoginRequest'
            examples:
              request:
                summary: Вход
                value:
                  username: '9991234567'
                  password: MyPass123
      responses:
        '200':
          description: Успешный вход
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthTokensResponse'
        '400':
          description: Неверные учётные данные или ошибка схемы
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/access-token:
    post:
      summary: Обмен access-токена
      operationId: exchangeAccessToken
      description: Получить обновлённый access-токен и данные пользователя по действующему access-токену.
      tags:
        - Аутентификация
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccessTokenRequest'
      responses:
        '200':
          description: Новый access-токен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessTokenResponse'
        '400':
          description: Невалидный токен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/refresh-token:
    post:
      summary: Обновление токенов по refresh-токену
      operationId: refreshToken
      description: Получить новую пару access+refresh токенов по действующему refresh-токену.
      tags:
        - Аутентификация
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefreshTokenRequest'
      responses:
        '200':
          description: Новые токены
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthTokensResponse'
        '400':
          description: Невалидный refresh-токен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── Company Management API (/api/v1) ────────────────────────────────────────
  # Все эндпоинты ниже требуют ApiKeyAuth (Bearer mm_…).
  # companyId берётся из ключа — не передавать в теле или URL.

  /v1/me:
    get:
      summary: Информация о ключе
      operationId: apiV1Me
      description: |
        Возвращает companyId, название компании и список scope текущего ключа.
        Эндпоинт не требует дополнительных scope — доступен любому валидному ключу.
        Используйте для проверки ключа и обнаружения доступных возможностей.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Данные ключа
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyMeResponse'
              examples:
                success:
                  summary: Успешный ответ
                  value:
                    companyId: 78910
                    companyName: Кофейня «Утро»
                    scopes:
                      - gifts:read
                      - gifts:write
                      - promotions:read
                      - promotions:write
                      - stats:read
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
              examples:
                invalid:
                  summary: Невалидный ключ
                  value:
                    error: invalid_api_key
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/gifts:
    get:
      summary: Список подарков компании
      operationId: apiV1GiftsFind
      description: |
        Возвращает список подарков компании с пагинацией.
        Требует scope: gifts:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: page
          in: query
          description: Номер страницы (начиная с 0)
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
        - name: limit
          in: query
          description: Количество записей на страницу (1–100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
          example: 10
        - name: sort
          in: query
          description: Поле сортировки
          schema:
            type: string
            enum:
              - expired_at
              - started_at
              - gift_id
              - title
              - amount
            default: expired_at
          example: expired_at
        - name: order
          in: query
          description: Направление сортировки
          schema:
            type: string
            enum:
              - asc
              - desc
            default: desc
          example: desc
        - name: search
          in: query
          description: Поиск по названию подарка
          schema:
            type: string
          example: кофе
      responses:
        '200':
          description: Список подарков
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GiftListResponse'
        '400':
          description: Некорректные параметры (невалидный sort/order/page/limit)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope gifts:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
              examples:
                noScope:
                  summary: Нет scope
                  value:
                    error: insufficient_scope
                    required: gifts:read
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      summary: Создать подарок
      operationId: apiV1GiftsCreate
      description: |
        Создаёт новый подарок для компании и отправляет его на модерацию.
        Требует scope: gifts:write.
        companyId берётся из ключа — не передавать в теле.

        Изображения (поле pictures) передаются инлайном как base64 data-URI
        прямо в теле запроса — отдельного эндпоинта загрузки файлов нет.
        Сервер сохраняет изображение самостоятельно.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GiftCreateRequest'
            examples:
              certificate:
                summary: Сертификат на 500 рублей
                value:
                  title: Сертификат 500 рублей
                  type: certificate
                  valuePrice: 500
                  description: Сертификат на любой заказ
                  condition: При покупке от 1000 рублей
                  startedAt: 1720000000000
                  expiredAt: 1730000000000
      responses:
        '200':
          description: Подарок создан (отправлен на модерацию)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GiftBase'
        '400':
          description: Некорректные данные подарка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope gifts:write)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/gifts/{giftId}:
    get:
      summary: Получить подарок по ID
      operationId: apiV1GiftsGet
      description: |
        Возвращает данные конкретного подарка компании.
        Требует scope: gifts:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: giftId
          in: path
          required: true
          description: >-
            ID подарка — поле `id` из ответа create/list (равно giftId).
            Подставляйте `id` из ответа, не promotionId.
          schema:
            type: integer
          example: 1001
      responses:
        '200':
          description: Данные подарка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GiftBase'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope gifts:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      summary: Обновить подарок
      operationId: apiV1GiftsUpdate
      description: |
        Обновляет существующий подарок и отправляет на повторную модерацию.
        Требует scope: gifts:write.
        giftId берётся строго из пути — нельзя передать в теле.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: giftId
          in: path
          required: true
          description: >-
            ID подарка для обновления — поле `id` из ответа create/list
            (равно giftId). Подставляйте `id` из ответа, не promotionId.
          schema:
            type: integer
          example: 1001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GiftCreateRequest'
      responses:
        '200':
          description: Подарок обновлён
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GiftBase'
        '400':
          description: Некорректные данные
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope gifts:write)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/gifts/{giftId}/stats:
    get:
      summary: Статистика по подарку
      operationId: apiV1GiftsStats
      description: |
        Возвращает агрегированную статистику использования подарка.
        Требует scope: stats:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: giftId
          in: path
          required: true
          description: >-
            ID подарка — поле `id` из ответа create/list (равно giftId).
            Подставляйте `id` из ответа, не promotionId.
          schema:
            type: integer
          example: 1001
      responses:
        '200':
          description: Статистика по подарку
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GiftStatsResponse'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope stats:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/promotions/one-time-gift:
    post:
      summary: Создать акцию «Разовый подарок»
      operationId: apiV1PromotionsOneTimeGiftCreate
      description: |
        Создаёт акцию типа «разовый подарок» — отправляет сертификат выбранным
        держателям карт. Требует scope: promotions:write.

        Ограничения:
        - Подарок (giftId) должен быть типа certificate и иметь достаточное количество.
        - cardCodes — список кодов карт (каждый на новой строке или через запятую), до 1000.
        - giftAmount должен быть ≥ числа карт в cardCodes.
        - Подарок не должен истекать раньше, чем первая партия клиентов успеет его получить.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PromotionGiftsCreateRequest'
            examples:
              create:
                summary: Создание акции
                value:
                  giftId: 1001
                  title: Ноябрьская акция
                  description: Дарим кофе новым клиентам
                  giftAmount: 100
                  giftExpires: 30
                  giftOfferExpires: 7
                  cardCodes: "1234567890\n0987654321"
      responses:
        '200':
          description: Акция создана
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PromotionGiftsCreateResponse'
              examples:
                success:
                  summary: Акция создана
                  value:
                    status: success
                    data:
                      promotionGiftsId: 501
                      title: Ноябрьская акция
                giftNotFound:
                  summary: Подарок не найден
                  value:
                    status: error
                    message: GiftNotFound
        '400':
          description: Некорректные параметры
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:write)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    get:
      summary: Список акций «Разовый подарок»
      operationId: apiV1PromotionsOneTimeGiftFind
      description: |
        Возвращает список акций типа «разовый подарок» с пагинацией.
        Требует scope: promotions:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
        - name: sort
          in: query
          schema:
            type: string
            enum:
              - createdAt
              - expiredAt
              - giftAmount
              - giftExpires
              - giftOfferExpires
              - promotionGiftsId
              - promotionId
              - startedAt
              - statGiftsClaimed
              - statGiftsExpired
              - statGiftsRejected
              - statGiftsSent
              - statGiftsUsed
              - title
            default: createdAt
        - name: order
          in: query
          schema:
            type: string
            enum:
              - asc
              - desc
            default: desc
        - name: search
          in: query
          description: Поиск по названию
          schema:
            type: string
      responses:
        '200':
          description: Список акций
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PromotionGiftsFindResponse'
        '400':
          description: Некорректные параметры
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/promotions/one-time-gift/{id}:
    get:
      summary: Получить акцию «Разовый подарок» по ID
      operationId: apiV1PromotionsOneTimeGiftGet
      description: |
        Возвращает данные конкретной акции «разовый подарок».
        Требует scope: promotions:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: >-
            ID акции — поле `id` из ответа create/list (равно promotionGiftsId).
            Используйте `id`, а не promotionId.
          schema:
            type: integer
          example: 501
      responses:
        '200':
          description: Данные акции
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PromotionGiftsGetResponse'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/promotions/one-time-gift/{id}/close:
    post:
      summary: Закрыть акцию «Разовый подарок»
      operationId: apiV1PromotionsOneTimeGiftClose
      description: |
        Досрочно останавливает акцию «разовый подарок» (архивирует акцию — дата
        окончания переносится на вчерашний день, дальнейшая раздача подарка
        прекращается). Требует scope: promotions:write.

        ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ: для акций этого типа метод сейчас не может
        фактически остановить акцию и при любом вызове отвечает HTTP 400
        (business_error), независимо от параметров запроса или состояния акции.
        Ответ 200 ниже описан как часть контракта, но фактически недостижим до
        устранения этого ограничения.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: >-
            ID акции — поле `id` из ответа create/list (равно promotionGiftsId).
            Используйте `id`, а не promotionId.
          schema:
            type: integer
          example: 501
      responses:
        '200':
          description: >-
            Акция остановлена. Фактически недостижимо для акций этого типа —
            см. известное ограничение в описании метода выше.
          content:
            application/json:
              schema:
                type: string
                example: OK
        '400':
          description: >-
            Акцию не удалось остановить. Для акций этого типа это единственный
            фактический результат вызова (см. известное ограничение выше) —
            возвращается независимо от того, активна акция, уже остановлена
            или её период истёк.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                cannotStop:
                  summary: Акцию не удалось остановить (текущее ограничение метода)
                  value:
                    error: business_error
                    message: Не удалось выполнить операцию, проверьте параметры и попробуйте ещё раз.
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:write)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '404':
          description: Акция не найдена или принадлежит другой компании
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'

  /v1/promotions/gift-for-coins:
    post:
      summary: Создать акцию «Подарок за покупку»
      operationId: apiV1PromotionsGiftForCoinsCreate
      description: |
        Создаёт акцию типа «подарок за покупку» (promocode) — клиент тратит
        накопленные монеты и получает подарок.
        Требует scope: promotions:write.

        Поля pictureSmall и pictureMiddle обязательны при создании и передаются
        инлайном как base64 data-URI. Сервер сохраняет изображения самостоятельно.

        В личном кабинете компании эта механика называется «Подарок за покупку».
        Отдельный раздел ЛКБ «Подарок за монеты» (обмен накопленных монет клиента
        на подарок) — другая механика и через /api/v1 недоступен.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PromocodeCreateRequest'
            examples:
              create:
                summary: Создание акции
                value:
                  title: Кофе за монеты
                  description: Обменяй 100 монет на любой кофе
                  giftId: 1001
                  amount: 50
                  startDate: '2026-09-01'
                  endDate: '2026-11-30'
                  moneySpend: 100
      responses:
        '200':
          description: Результат создания акции
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PromocodeCreateResponse'
              examples:
                success:
                  summary: Акция создана
                  value:
                    status: successful
                giftNotFound:
                  summary: Подарок не найден
                  value:
                    status: GiftNotFound
        '400':
          description: Некорректные параметры
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:write)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    get:
      summary: Список акций «Подарок за покупку»
      operationId: apiV1PromotionsGiftForCoinsFind
      description: |
        Возвращает список акций типа «подарок за покупку» с пагинацией.
        Требует scope: promotions:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
        - name: sort
          in: query
          schema:
            type: string
            enum:
              - company_id
              - expiredAt
              - startedAt
              - title
            default: expiredAt
        - name: order
          in: query
          schema:
            type: string
            enum:
              - asc
              - desc
            default: desc
        - name: search
          in: query
          description: Поиск по названию
          schema:
            type: string
      responses:
        '200':
          description: Список акций
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PromocodeFindResponse'
        '400':
          description: Некорректные параметры
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/promotions/gift-for-coins/{id}:
    get:
      summary: Получить акцию «Подарок за покупку» по ID
      operationId: apiV1PromotionsGiftForCoinsGet
      description: |
        Возвращает данные конкретной акции «подарок за покупку».
        Требует scope: promotions:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: >-
            ID акции — поле `id` из ответа create/list (равно promotionPromocodeId).
            Используйте `id`, а не promotionId.
          schema:
            type: integer
          example: 301
      responses:
        '200':
          description: Данные акции
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PromocodeFindResponse'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyError'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/promotions/gift-for-coins/{id}/close:
    post:
      summary: Закрыть акцию «Подарок за покупку»
      operationId: apiV1PromotionsGiftForCoinsClose
      description: |
        Досрочно останавливает акцию «подарок за покупку» (архивирует акцию — дата
        окончания переносится на вчерашний день, промокод перестаёт выдаваться).
        Требует scope: promotions:write.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: >-
            ID акции — поле `id` из ответа create/list (равно promotionPromocodeId).
            Используйте `id`, а не promotionId.
          schema:
            type: integer
          example: 301
      responses:
        '200':
          description: Акция остановлена
          content:
            application/json:
              schema:
                type: string
                example: OK
        '400':
          description: Акцию нельзя остановить — она уже остановлена или её период уже истёк
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                alreadyStopped:
                  summary: Акция уже остановлена/истекла
                  value:
                    error: business_error
                    message: Не удалось выполнить операцию, проверьте параметры и попробуйте ещё раз.
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:write)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '404':
          description: Акция не найдена или принадлежит другой компании
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'

  /v1/promotions/smart-ad:
    post:
      summary: Создать акцию «Smart реклама»
      operationId: apiV1PromotionsSmartAdCreate
      description: |
        Создаёт акцию типа «Smart реклама» (target promotion) — отправляет сертификат
        конкретным держателям карт по списку кодов. Требует scope: promotions:write.

        ОГРАНИЧЕНИЕ: через API-ключ поддерживается ТОЛЬКО targetMode=card_code.
        Любое другое значение вернёт HTTP 400 (error=invalid_target_mode).
        Режим target_schema (автовыборка по профилю клиента) доступен только через ЛКБ.

        ПРАВИЛО ДАТ ПОДАРКА: срок действия подарка (expiredAt) должен заканчиваться
        минимум на 3 дня ПОЗЖЕ даты окончания акции — иначе ошибка gift_after_share_time.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SmartAdCreateRequest'
            examples:
              create:
                summary: Создание Smart рекламы
                value:
                  title: Персональная акция для VIP
                  description: Персональный подарок для постоянных клиентов
                  giftId: 1001
                  giftAmount: 10
                  giftExpires: 30
                  giftOfferExpires: 7
                  targetMode: card_code
                  cardCodes: "1234567890\n0987654321"
      responses:
        '200':
          description: Акция создана
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SmartAdItem'
        '400':
          description: Некорректные параметры или неподдерживаемый targetMode
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                invalidTargetMode:
                  summary: Неподдерживаемый targetMode
                  value:
                    error: invalid_target_mode
                    message: Через API-ключ поддерживается только режим targetMode=card_code.
                giftAfterShareTime:
                  summary: Срок подарка не перекрывает акцию + 3 дня
                  value:
                    error: gift_after_share_time
                    message: Подарок должен действовать минимум на 3 дня дольше даты окончания акции.
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:write)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
    get:
      summary: Список акций «Smart реклама»
      operationId: apiV1PromotionsSmartAdFind
      description: |
        Возвращает список акций типа «Smart реклама» с пагинацией.
        Требует scope: promotions:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
        - name: sort
          in: query
          schema:
            type: string
            enum:
              - createdAt
              - giftAmount
              - title
            default: createdAt
        - name: order
          in: query
          schema:
            type: string
            enum:
              - asc
              - desc
            default: desc
        - name: search
          in: query
          description: Поиск по названию
          schema:
            type: string
      responses:
        '200':
          description: Список акций Smart рекламы
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SmartAdFindResponse'
        '400':
          description: Некорректные параметры
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'

  /v1/promotions/smart-ad/{id}:
    get:
      summary: Получить акцию «Smart реклама» по ID
      operationId: apiV1PromotionsSmartAdGet
      description: |
        Возвращает данные конкретной акции «Smart реклама».
        Требует scope: promotions:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: >-
            ID акции — поле `id` из ответа create/list. Используйте `id`,
            а не promotionId.
          schema:
            type: integer
          example: 401
      responses:
        '200':
          description: Данные акции
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SmartAdItem'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '404':
          description: Акция не найдена
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'

  /v1/promotions/smart-ad/{id}/close:
    post:
      summary: Закрыть акцию «Smart реклама»
      operationId: apiV1PromotionsSmartAdClose
      description: |
        Досрочно останавливает акцию «Smart реклама» (архивирует акцию — дата
        окончания переносится на вчерашний день, дальнейшая раздача подарка
        прекращается). Требует scope: promotions:write.

        ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ: для акций этого типа метод сейчас не может
        фактически остановить акцию и при любом вызове отвечает HTTP 400
        (business_error), независимо от параметров запроса или состояния акции.
        Ответ 200 ниже описан как часть контракта, но фактически недостижим до
        устранения этого ограничения.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: >-
            ID акции — поле `id` из ответа create/list (равно promotionTargetId).
            Используйте `id`, а не promotionId.
          schema:
            type: integer
          example: 401
      responses:
        '200':
          description: >-
            Акция остановлена. Фактически недостижимо для акций этого типа —
            см. известное ограничение в описании метода выше.
          content:
            application/json:
              schema:
                type: string
                example: OK
        '400':
          description: >-
            Акцию не удалось остановить. Для акций этого типа это единственный
            фактический результат вызова (см. известное ограничение выше) —
            возвращается независимо от того, активна акция, уже остановлена
            или её период истёк.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                cannotStop:
                  summary: Акцию не удалось остановить (текущее ограничение метода)
                  value:
                    error: business_error
                    message: Не удалось выполнить операцию, проверьте параметры и попробуйте ещё раз.
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:write)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '404':
          description: Акция не найдена или принадлежит другой компании
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'

  # ── Referral program (recommendation) ──────────────────────────────────────

  /v1/promotions/recommendation/active:
    get:
      summary: Проверить наличие активной процентной программы
      operationId: apiV1PromotionsRecommendationActive
      description: |
        Возвращает {active: boolean} — есть ли у компании активная процентная (type=percent)
        акция «Рекомендации» (реферальная механика). Максимум 1 активная percent-акция на компанию одновременно.
        Требует scope: promotions:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Статус наличия активной процентной программы
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecommendationActiveResponse'
              examples:
                noActive:
                  summary: Нет активной
                  value:
                    active: false
                hasActive:
                  summary: Есть активная
                  value:
                    active: true
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'

  /v1/promotions/recommendation:
    post:
      summary: Создать акцию «Рекомендации»
      operationId: apiV1PromotionsRecommendationCreate
      description: |
        Создаёт акцию «Рекомендации» (реферальная механика) компании. Два типа:
        - type=percent — процент от покупок рефералов; только для компаний с бонусной системой;
          максимум 1 активная percent-программа на компанию (проверить через /active).
        - type=once-gift — единовременный подарок рефереру и/или новому клиенту при первой покупке.

        Требует scope: promotions:write.

        ПРАВИЛО ДАТ ПОДАРКА: срок действия подарков (expiredAt) должен заканчиваться
        минимум на 3 дня ПОЗЖЕ даты окончания программы (endDate) — иначе ошибка gift_after_share_time.

        Изображения pictureSmall и pictureMiddle передаются инлайном как base64 data-URI.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/RecommendationCreatePercentRequest'
                - $ref: '#/components/schemas/RecommendationCreateOnceGiftRequest'
              discriminator:
                propertyName: type
            examples:
              percent:
                summary: Процентная программа
                value:
                  type: percent
                  condition: Приведи друга и получай 5% с каждой его покупки
                  giftId: 2001
                  levels:
                    '1': 5
                    '2': 3
                    '3': 1
                  startDate: '2026-09-01'
                  endDate: '2027-09-01'
                  pictureSmall: "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
                  pictureMiddle: "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
              onceGift:
                summary: Единовременный подарок
                value:
                  type: once-gift
                  condition: При первой покупке нового клиента оба получают подарок
                  startDate: '2026-09-01'
                  endDate: '2027-09-01'
                  recipientMinBuySum: 500
                  recipient:
                    giftId: 2002
                    giftAmount: 1
                    expiredDays: 30
                  sender:
                    giftId: 2003
                    giftAmount: 1
                    expiredDays: 30
                  pictureSmall: "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
                  pictureMiddle: "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
      responses:
        '200':
          description: Программа создана
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecommendationItem'
        '400':
          description: Некорректные параметры или бизнес-ошибка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                percentAlreadyActive:
                  summary: Уже есть активная процентная программа
                  value:
                    error: recommendation_percent_already_active
                    message: У компании уже есть активная процентная реферальная программа. Завершите текущую перед созданием новой.
                giftAfterShareTime:
                  summary: Срок подарка не перекрывает программу + 3 дня
                  value:
                    error: gift_after_share_time
                    message: Подарок должен действовать минимум на 3 дня дольше даты окончания акции.
                notBonusCompany:
                  summary: Компания без бонусной системы
                  value:
                    error: recommendation_not_bonus_company
                    message: Процентная реферальная программа доступна только компаниям с бонусной системой.
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:write)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
    get:
      summary: Список акций «Рекомендации»
      operationId: apiV1PromotionsRecommendationFind
      description: |
        Возвращает список акций «Рекомендации» компании с пагинацией.
        Требует scope: promotions:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
        - name: sort
          in: query
          schema:
            type: string
            enum:
              - endDate
              - expiredAt
              - gift
              - participantsCount
              - recommendationId
              - startDate
              - startedAt
              - status
              - stoppedAt
              - type
            default: recommendationId
        - name: order
          in: query
          schema:
            type: string
            enum:
              - asc
              - desc
            default: desc
      responses:
        '200':
          description: Список программ
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecommendationFindResponse'
        '400':
          description: Некорректные параметры
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'

  /v1/promotions/recommendation/{id}:
    post:
      summary: Обновить акцию «Рекомендации»
      operationId: apiV1PromotionsRecommendationUpdate
      description: |
        Обновляет параметры акции «Рекомендации». ID берётся из пути — нельзя передать в теле.
        Тип программы (type) изменить нельзя — ошибка recommendation_cannot_change_type.
        Требует scope: promotions:write.

        ПРАВИЛО ДАТ ПОДАРКА: срок действия подарков должен заканчиваться минимум
        на 3 дня ПОЗЖЕ обновлённой даты окончания программы (endDate).
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: >-
            ID акции «Рекомендации» — поле `id` из ответа create/list
            (равно recommendationId). Используйте `id`, а не promotionId.
          schema:
            type: integer
          example: 601
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/RecommendationCreatePercentRequest'
                - $ref: '#/components/schemas/RecommendationCreateOnceGiftRequest'
              discriminator:
                propertyName: type
      responses:
        '200':
          description: Программа обновлена
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecommendationItem'
        '400':
          description: Некорректные параметры или бизнес-ошибка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                cannotChangeType:
                  summary: Нельзя изменить тип программы
                  value:
                    error: recommendation_cannot_change_type
                    message: Нельзя изменить тип уже созданной программы.
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:write)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '404':
          description: Программа не найдена
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
    get:
      summary: Получить акцию «Рекомендации» по ID
      operationId: apiV1PromotionsRecommendationGet
      description: |
        Возвращает данные конкретной акции «Рекомендации».
        Требует scope: promotions:read.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: >-
            ID акции «Рекомендации» — поле `id` из ответа create/list
            (равно recommendationId). Используйте `id`, а не promotionId.
          schema:
            type: integer
          example: 601
      responses:
        '200':
          description: Данные программы
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecommendationItem'
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:read)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '404':
          description: Программа не найдена
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'

  /v1/promotions/recommendation/{id}/close:
    post:
      summary: Закрыть акцию «Рекомендации»
      operationId: apiV1PromotionsRecommendationClose
      description: |
        Досрочно завершает акцию «Рекомендации». Требует scope: promotions:write.

        МЕТОД ИДЕМПОТЕНТЕН И НЕ СООБЩАЕТ, БЫЛО ЛИ РЕАЛЬНОЕ ИЗМЕНЕНИЕ: отвечает
        HTTP 200 даже если акция с указанным `id` не найдена, принадлежит другой
        компании или уже была закрыта ранее. Успешный ответ подтверждает только
        то, что запрос принят и обработан — не то, что состояние акции
        изменилось. Проверяйте фактический статус акции через GET по тому же id.
      tags:
        - Company Management API
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: >-
            ID акции «Рекомендации» — поле `id` из ответа create/list
            (равно recommendationId). Используйте `id`, а не promotionId.
          schema:
            type: integer
          example: 601
      responses:
        '200':
          description: >-
            Запрос принят и обработан (не гарантирует, что акция реально была
            найдена/закрыта — см. описание метода выше).
          content:
            application/json:
              schema:
                type: string
                example: OK
        '401':
          description: API-ключ отсутствует, невалиден или отозван
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '402':
          description: Ключ неактивен — нет активного пробного периода или оплаченной подписки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
              examples:
                subscriptionRequired:
                  summary: Нет ни подписки, ни пробного периода
                  value:
                    error: subscription_required
                    message: API-ключи работают только при активном пробном периоде или оплаченной подписке. Активируйте пробный период или оформите подписку.
                    entitlement: no_subscription
                trialExpired:
                  summary: Пробный период закончился
                  value:
                    error: trial_expired
                    message: Пробный период закончился. Оформите подписку, чтобы возобновить работу API-ключей.
                    entitlement: trial_blocked
                subscriptionInactive:
                  summary: Подписка истекла или не оплачена
                  value:
                    error: subscription_inactive
                    message: Подписка неактивна. Оплатите, чтобы возобновить работу API-ключей.
                    entitlement: blocked
        '403':
          description: Недостаточно прав (требуется scope promotions:write)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
        '500':
          description: Сбой сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiV1Error'
