openapi: "3.0.3"
info:
  title: unotice-openapi-client - HTTP API
  description: |
    REST API для отправки нотификаций через platform-notification-service.
    Авторизация через OAuth2 JWT (Keycloak). Клиент идентифицируется по claim client_id из токена.
    Webhook привязан к клиенту из токена (один клиент = один webhook URL).

    Rate-limit: 100 запросов в минуту на интеграцию (по client_id из JWT). При превышении — 429.

    Webhook callback: после того как нотификация достигнет терминального статуса, сервис отправляет POST-запрос на URL,
    зарегистрированный через POST /api/v1/webhooks. Формат payload-а и retry-политика описаны в секции callbacks этого endpoint-а
    (схема NotificationCallbackEvent).
  version: "1.0"
servers:
  - url: https://std.seven.tech/unotice-openapi-client
    description: 'Production'

tags:
  - name: notifications
    description: 'Отправка нотификаций'
  - name: webhook
    description: 'Управление webhook'

security:
  - BearerAuth: []

paths:
  /api/v1/notifications:
    post:
      tags:
        - notifications
      summary: 'Отправка нотификации'
      description: |
        Требуется scope notification. Клиент определяется из JWT токена.

        Идемпотентность: повторный запрос с тем же `externalId` в рамках одного клиента
        вернёт тот же `notificationId` (без повторной отправки).
        При 500/503 состояние доставки неопределено — безопасно ретраить только если
        указан `externalId`.
      operationId: sendNotification
      security:
        - BearerAuth: []
      requestBody:
        $ref: '#/components/requestBodies/SendNotificationRequest'
      responses:
        202:
          $ref: '#/components/responses/SendNotificationResponse'
        400:
          $ref: '#/components/responses/ErrorResponse'
        401:
          $ref: '#/components/responses/ErrorResponse'
        403:
          $ref: '#/components/responses/ErrorResponse'
        429:
          $ref: '#/components/responses/RateLimitErrorResponse'
        500:
          $ref: '#/components/responses/ErrorResponse'
        503:
          $ref: '#/components/responses/ErrorResponse'

  /api/v1/webhooks:
    post:
      tags:
        - webhook
      summary: 'Создание webhook'
      description: 'Регистрирует URL для получения callback-ов о результатах доставки. Привязывается к клиенту из JWT токена (один клиент = один webhook). Ошибка 400, если webhook уже зарегистрирован (используйте PUT для обновления). Требуется scope webhook.'
      operationId: createWebhook
      security:
        - BearerAuth: []
      requestBody:
        $ref: '#/components/requestBodies/CreateWebhookRequest'
      responses:
        200:
          description: OK
        400:
          $ref: '#/components/responses/ErrorResponse'
        401:
          $ref: '#/components/responses/ErrorResponse'
        403:
          $ref: '#/components/responses/ErrorResponse'
        404:
          $ref: '#/components/responses/ErrorResponse'
        409:
          $ref: '#/components/responses/ErrorResponse'
        429:
          $ref: '#/components/responses/RateLimitErrorResponse'
        500:
          $ref: '#/components/responses/ErrorResponse'
      callbacks:
        onNotificationEvent:
          '{$request.body#/url}':
            post:
              summary: 'Отправка результата доставки нотификации на webhook URL клиента'
              description: |
                Срабатывает, когда нотификация достигает терминального статуса (DELIVERED или FAILED).
                Retry-политика: при ответе 5xx, 408, 429 или сетевой ошибке callback ретраится до 8 раз
                с exponential backoff (waits 10s, 20s, 40s, 80s, 160s, 320s, 640s, 1280s; полное окно ≈ 42 минуты).
                При остальных 4xx-ответах событие помечается permanent и не ретраится.
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      $ref: '#/components/schemas/NotificationCallbackEvent'
              responses:
                '2XX':
                  description: 'Клиент успешно принял событие.'
                '4XX':
                  description: 'Permanent failure: событие удаляется без retry (кроме 408 и 429).'
                '5XX':
                  description: 'Transient failure: событие уходит в retry-очередь (до 8 попыток).'
    put:
      tags:
        - webhook
      summary: 'Обновление webhook'
      description: 'Обновляет URL webhook-а текущего клиента (из JWT токена). Ошибка 400, если webhook ещё не создан (используйте POST для создания). Требуется scope webhook.'
      operationId: updateWebhook
      security:
        - BearerAuth: []
      requestBody:
        $ref: '#/components/requestBodies/CreateWebhookRequest'
      responses:
        200:
          description: OK
        400:
          $ref: '#/components/responses/ErrorResponse'
        401:
          $ref: '#/components/responses/ErrorResponse'
        403:
          $ref: '#/components/responses/ErrorResponse'
        404:
          $ref: '#/components/responses/ErrorResponse'
        409:
          $ref: '#/components/responses/ErrorResponse'
        429:
          $ref: '#/components/responses/RateLimitErrorResponse'
        500:
          $ref: '#/components/responses/ErrorResponse'
    delete:
      tags:
        - webhook
      summary: 'Удаление webhook'
      description: 'Удаляет webhook текущего клиента (из JWT токена). Callback-и перестанут отправляться. Требуется scope webhook.'
      operationId: deleteWebhook
      security:
        - BearerAuth: []
      responses:
        200:
          description: OK
        400:
          $ref: '#/components/responses/ErrorResponse'
        401:
          $ref: '#/components/responses/ErrorResponse'
        403:
          $ref: '#/components/responses/ErrorResponse'
        404:
          $ref: '#/components/responses/ErrorResponse'
        409:
          $ref: '#/components/responses/ErrorResponse'
        429:
          $ref: '#/components/responses/RateLimitErrorResponse'
        500:
          $ref: '#/components/responses/ErrorResponse'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'OAuth2 JWT токен из Keycloak. Требуемые scopes проверяются на стороне сервиса: SCOPE_notification (отправка нотификаций), SCOPE_webhook (управление webhook).'

  requestBodies:
    SendNotificationRequest:
      description: 'Запрос на отправку нотификации'
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SendNotificationRequest'
    CreateWebhookRequest:
      description: 'Запрос на создание webhook'
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/CreateWebhookRequest'

  schemas:
    SendNotificationRequest:
      title: "SendNotificationRequest"
      description: "Запрос на отправку нотификации"
      required:
        - recipient
        - notificationText
      properties:
        recipient:
          $ref: '#/components/schemas/Recipient'
        notificationText:
          type: string
          minLength: 1
          maxLength: 4096
          description: "Текст нотификации (до 4096 символов)"
        externalId:
          type: string
          description: "Внешний идентификатор нотификации (для идемпотентности)"

    Recipient:
      title: "Recipient"
      description: "Получатель нотификации (телефон в формате MSISDN)"
      required:
        - value
      properties:
        value:
          type: string
          description: "Номер телефона получателя в формате MSISDN (например, 79991234567)"

    SendNotificationResult:
      title: "SendNotificationResult"
      description: "Результат отправки нотификации"
      required:
        - notificationId
      properties:
        notificationId:
          type: string
          format: uuid
          description: "Идентификатор созданной нотификации"

    CreateWebhookRequest:
      title: "CreateWebhookRequest"
      description: "Запрос на создание webhook"
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          description: "Адрес сервера на который нужно передавать колбэки"

    Error:
      title: "Error"
      type: object
      properties:
        errorCode:
          type: integer
          description: "Код ошибки"
        errorDescription:
          type: string
          description: "Описание ошибки"

    NotificationCallbackEvent:
      title: "NotificationCallbackEvent"
      description: "Корневой объект callback-события, который сервис POST-ом отправляет на webhook URL клиента."
      type: object
      required:
        - eventType
        - notificationStatusChanged
      properties:
        eventType:
          type: string
          enum: [ NOTIFICATION_STATUS_CHANGED ]
          description: "Тип события. Сейчас единственное значение — NOTIFICATION_STATUS_CHANGED."
        notificationStatusChanged:
          $ref: '#/components/schemas/NotificationStatusChangedEvent'

    NotificationStatusChangedEvent:
      title: "NotificationStatusChangedEvent"
      description: "Данные об изменении статуса нотификации"
      type: object
      required:
        - notificationId
        - status
      properties:
        notificationId:
          type: string
          format: uuid
          description: "Идентификатор нотификации, который был возвращён POST /api/v1/notifications"
        status:
          type: string
          enum: [ DELIVERED, FAILED ]
          description: |
            Терминальный статус доставки.
            DELIVERED — сообщение успешно доставлено получателю.
            FAILED — нотификация не доставлена (cascade исчерпал все каналы либо адаптер вернул NOT_DELIVERED / ACCOUNT_BANNED / UNKNOWN — все они сворачиваются в FAILED). Причина в errors[].
        deliveredAt:
          type: string
          format: date-time
          description: "Дата успешной доставки (присутствует только при status=DELIVERED)"
        errors:
          type: array
          description: "Массив ошибок (присутствует только при status=FAILED)"
          items:
            $ref: '#/components/schemas/NotificationDeliveryError'

    NotificationDeliveryError:
      title: "NotificationDeliveryError"
      description: "Информация об ошибке доставки"
      type: object
      required:
        - code
        - description
      properties:
        code:
          type: string
          description: "Код ошибки (например, CASCADE_FAILED)"
        description:
          type: string
          description: "Человекочитаемое описание ошибки"

  responses:
    SendNotificationResponse:
      description: 'Нотификация принята'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SendNotificationResult'
    ErrorResponse:
      description: 'Ошибка'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimitErrorResponse:
      description: 'Превышен rate-limit (100 запросов в минуту на интеграцию).'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
