openapi: "3.1.0"
info:
  title: Telephony MiniApp — Входящие события
  description: >
    Входящие события от внешнего приложения (MiniApp → Acloud).
    Внешнее приложение отправляет POST-запрос на endpoint хука:
    POST /external/rest/telephony/hook/mini_app/{account_id}/{uuid}
    где {account_id} — ID аккаунта, {uuid} — уникальный идентификатор интеграции
    (возвращается в ответе API telephony/create).
  version: "1.0.0"

webhooks:

  call.init:
    post:
      summary: Инициализация звонка
      description: >
        Отправляется внешним приложением при появлении нового звонка (до соединения).
        Для входящих звонков Acloud возвращает решение о маршрутизации — на какого
        сотрудника переадресовать вызов. Для исходящих — подтверждение приёма.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - call.init
                payload:
                  $ref: '#/components/schemas/CallInitPayload'
            examples:
              call_init_inbound:
                summary: Входящий звонок
                value:
                  method: call.init
                  payload:
                    call_id: call_abc123
                    from: "+79990001234"
                    to: "101"
                    direction: in
                    timestamp: "2026-04-09T10:30:00Z"
              call_init_outbound:
                summary: Исходящий звонок
                value:
                  method: call.init
                  payload:
                    call_id: call_def456
                    from: "101"
                    to: "+79995556677"
                    direction: out
                    timestamp: "2026-04-09T11:00:00Z"
      responses:
        "200":
          description: >
            Событие успешно обработано. Для входящих звонков содержит решение о маршрутизации.
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    $ref: '#/components/schemas/RoutingResult'
              examples:
                redirect:
                  summary: Переадресация на сотрудника
                  value:
                    result:
                      action: redirect
                      target:
                        sip: "101"
                pass:
                  summary: Пропустить без переадресации
                  value:
                    result:
                      action: pass
        "400":
          description: Ошибка валидации (неверный формат или отсутствуют обязательные поля)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: >
            Интеграция деактивирована или аккаунт просрочен.
            Коды ошибок: `telephony_deactivated`, `account_expired`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "410":
          description: >
            Аккаунт или интеграция не найдены.
            Коды ошибок: `account_not_found`, `integration_not_found`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  call.start:
    post:
      summary: Звонок соединён
      description: >
        Отправляется внешним приложением в момент, когда звонок был принят
        и разговор начался.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - call.start
                payload:
                  $ref: '#/components/schemas/CallStartPayload'
            examples:
              call_start:
                summary: Звонок соединён
                value:
                  method: call.start
                  payload:
                    call_id: call_abc123
                    from: "+79990001234"
                    to: "101"
                    direction: in
                    timestamp: "2026-04-09T10:30:05Z"
      responses:
        "200":
          description: Событие успешно обработано
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    type: object
        "400":
          description: Ошибка валидации (неверный формат или отсутствуют обязательные поля)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: >
            Интеграция деактивирована или аккаунт просрочен.
            Коды ошибок: `telephony_deactivated`, `account_expired`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "410":
          description: >
            Аккаунт или интеграция не найдены.
            Коды ошибок: `account_not_found`, `integration_not_found`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  call.terminate:
    post:
      summary: Звонок завершён
      description: >
        Отправляется внешним приложением при завершении звонка.
        Содержит итоговый статус и длительность разговора.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - call.terminate
                payload:
                  $ref: '#/components/schemas/CallTerminatePayload'
            examples:
              call_terminate_success:
                summary: Успешно завершённый звонок
                value:
                  method: call.terminate
                  payload:
                    call_id: call_abc123
                    from: "+79990001234"
                    to: "101"
                    direction: in
                    timestamp: "2026-04-09T10:35:00Z"
                    call_duration: 295
                    status: terminated
              call_terminate_missed:
                summary: Пропущенный звонок
                value:
                  method: call.terminate
                  payload:
                    call_id: call_abc124
                    from: "+79991112233"
                    to: "102"
                    direction: in
                    timestamp: "2026-04-09T10:40:00Z"
                    call_duration: 0
                    status: missed
      responses:
        "200":
          description: Событие успешно обработано
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    type: object
        "400":
          description: Ошибка валидации (неверный формат или отсутствуют обязательные поля)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: >
            Интеграция деактивирована или аккаунт просрочен.
            Коды ошибок: `telephony_deactivated`, `account_expired`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "410":
          description: >
            Аккаунт или интеграция не найдены.
            Коды ошибок: `account_not_found`, `integration_not_found`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  call.record:
    post:
      summary: Информация о записи звонка
      description: >
        Отправляется внешним приложением после завершения звонка.
        Сообщает, была ли создана запись разговора.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - call.record
                payload:
                  $ref: '#/components/schemas/CallRecordPayload'
            examples:
              call_record_exists:
                summary: Запись создана
                value:
                  method: call.record
                  payload:
                    call_id: call_abc123
                    from: "+79990001234"
                    to: "101"
                    direction: in
                    timestamp: "2026-04-09T10:35:05Z"
                    has_record: true
              call_record_none:
                summary: Записи нет
                value:
                  method: call.record
                  payload:
                    call_id: call_abc125
                    from: "101"
                    to: "+79997778899"
                    direction: out
                    timestamp: "2026-04-09T12:00:10Z"
                    has_record: false
      responses:
        "200":
          description: Событие успешно обработано
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    type: object
        "400":
          description: Ошибка валидации (неверный формат или отсутствуют обязательные поля)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: >
            Интеграция деактивирована или аккаунт просрочен.
            Коды ошибок: `telephony_deactivated`, `account_expired`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "410":
          description: >
            Аккаунт или интеграция не найдены.
            Коды ошибок: `account_not_found`, `integration_not_found`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  call.info:
    post:
      summary: Обновление информации о звонке
      description: >
        Отправляется внешним приложением при изменении состояния звонка.
        Может приходить в произвольном порядке относительно других событий —
        система обрабатывает его идемпотентно.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - call.info
                payload:
                  $ref: '#/components/schemas/CallInfoPayload'
            examples:
              call_info_active:
                summary: Звонок активен
                value:
                  method: call.info
                  payload:
                    call_id: call_abc123
                    from: "+79990001234"
                    to: "101"
                    direction: in
                    timestamp: "2026-04-09T10:30:10Z"
                    status: active
              call_info_terminated:
                summary: Звонок завершён
                value:
                  method: call.info
                  payload:
                    call_id: call_abc123
                    from: "+79990001234"
                    to: "101"
                    direction: in
                    timestamp: "2026-04-09T10:35:10Z"
                    status: terminated
      responses:
        "200":
          description: Событие успешно обработано
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    type: object
        "400":
          description: Ошибка валидации (неверный формат или отсутствуют обязательные поля)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: >
            Интеграция деактивирована или аккаунт просрочен.
            Коды ошибок: `telephony_deactivated`, `account_expired`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "410":
          description: >
            Аккаунт или интеграция не найдены.
            Коды ошибок: `account_not_found`, `integration_not_found`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  integration.deactivate:
    post:
      summary: Деактивация интеграции
      description: >
        Отправляется внешним приложением для деактивации интеграции телефонии.
        После обработки интеграция переводится в неактивное состояние (active = false).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - integration.deactivate
                payload:
                  $ref: '#/components/schemas/IntegrationDeactivatePayload'
            examples:
              deactivate:
                summary: Деактивация интеграции
                value:
                  method: integration.deactivate
                  payload: {}
      responses:
        "200":
          description: Интеграция успешно деактивирована
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    type: object
        "410":
          description: >
            Интеграция не найдена.
            Код ошибки: `integration_not_found`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  schemas:

    IncomingEvent:
      type: object
      required:
        - method
        - payload
      properties:
        method:
          type: string
          enum:
            - call.init
            - call.start
            - call.terminate
            - call.record
            - call.info
            - integration.deactivate
          description: Тип события
        payload:
          oneOf:
            - $ref: '#/components/schemas/CallInitPayload'
            - $ref: '#/components/schemas/CallStartPayload'
            - $ref: '#/components/schemas/CallTerminatePayload'
            - $ref: '#/components/schemas/CallRecordPayload'
            - $ref: '#/components/schemas/CallInfoPayload'
            - $ref: '#/components/schemas/IntegrationDeactivatePayload'
          discriminator:
            propertyName: method

    # ─── Shared base ──────────────────────────────────────────────────────────

    CallBasePayload:
      type: object
      description: Базовые поля, присутствующие во всех событиях звонка
      required:
        - call_id
        - from
        - to
        - direction
        - timestamp
      properties:
        call_id:
          type: string
          description: Уникальный идентификатор звонка во внешней системе
          example: call_abc123
        from:
          type: string
          description: >
            Номер или внутренний номер (extension) источника звонка.
            Для входящих — номер телефона звонящего; для исходящих — extension сотрудника.
          example: "+79990001234"
        to:
          type: string
          description: >
            Номер или extension получателя звонка.
            Для входящих — extension сотрудника; для исходящих — номер телефона.
          example: "101"
        direction:
          type: string
          enum:
            - in
            - out
          description: Направление звонка. `in` — входящий, `out` — исходящий.
        timestamp:
          type: string
          description: Дата и время события в формате ISO 8601
          example: "2026-04-09T10:30:00Z"

    # ─── call.init ────────────────────────────────────────────────────────────

    CallInitPayload:
      allOf:
        - $ref: '#/components/schemas/CallBasePayload'
      description: Payload события инициализации звонка. Поля совпадают с базовым набором.

    # ─── call.start ───────────────────────────────────────────────────────────

    CallStartPayload:
      allOf:
        - $ref: '#/components/schemas/CallBasePayload'
      description: Payload события соединения звонка. Поля совпадают с базовым набором.

    # ─── call.terminate ───────────────────────────────────────────────────────

    CallTerminatePayload:
      allOf:
        - $ref: '#/components/schemas/CallBasePayload'
        - type: object
          required:
            - call_duration
            - status
          properties:
            call_duration:
              type: integer
              description: Длительность разговора в секундах
              example: 295
            status:
              type: string
              enum:
                - terminated
                - missed
              description: >
                Итоговый статус звонка.
                `terminated` — звонок был отвечен и завершён нормально;
                `missed` — звонок не был отвечен (пропущен).

    # ─── call.record ──────────────────────────────────────────────────────────

    CallRecordPayload:
      allOf:
        - $ref: '#/components/schemas/CallBasePayload'
        - type: object
          required:
            - has_record
          properties:
            has_record:
              type: boolean
              description: Флаг наличия записи разговора
              example: true
            record_url:
              type: string
              description: Ссылка на скачивание записи звонка
              example: https://домен/call_id
              nullable: true

    # ─── call.info ────────────────────────────────────────────────────────────

    CallInfoPayload:
      allOf:
        - $ref: '#/components/schemas/CallBasePayload'
        - type: object
          required:
            - status
          properties:
            status:
              type: string
              enum:
                - active
                - terminated
                - missed
              description: >
                Текущий статус звонка.
                `active` — звонок активен (соединён);
                `terminated` — звонок завершён успешно;
                `missed` — звонок не был отвечен.

    # ─── integration.deactivate ─────────────────────────────────────────────

    IntegrationDeactivatePayload:
      type: object
      description: >
        Payload события деактивации интеграции. Не содержит полей —
        сам факт отправки события означает запрос на деактивацию.

    # ─── Routing result ───────────────────────────────────────────────────────

    RoutingResult:
      type: object
      required:
        - action
      properties:
        action:
          type: string
          enum:
            - redirect
            - pass
          description: >
            Действие маршрутизации для входящего звонка.
            `redirect` — переадресовать на конкретного сотрудника;
            `pass` — пропустить без изменения маршрута.
        target:
          type: object
          description: "Цель переадресации. Присутствует только при action: redirect."
          required:
            - sip
          properties:
            sip:
              type: string
              description: SIP-адрес или extension сотрудника для переадресации
              example: "101"

    # ─── Error response ───────────────────────────────────────────────────────

    ErrorResponse:
      type: object
      required:
        - result
        - code
        - message
      properties:
        result:
          type: string
          enum:
            - error
        code:
          type: integer
          description: HTTP-код ответа
          example: 400
        error:
          type: string
          enum:
            - invalid_json
            - validation_error
            - event_not_supported
            - internal_error
            - account_not_found
            - integration_not_found
            - telephony_deactivated
            - account_expired
          description: >
            Машинно-читаемый код ошибки.
            `invalid_json` — тело запроса не является валидным JSON;
            `validation_error` — отсутствуют обязательные поля или неверный формат данных;
            `event_not_supported` — неизвестный тип события (method);
            `internal_error` — внутренняя ошибка сервера;
            `account_not_found` — аккаунт не существует;
            `integration_not_found` — интеграция телефонии не найдена;
            `telephony_deactivated` — интеграция телефонии деактивирована;
            `account_expired` — аккаунт просрочен, интеграция автоматически деактивирована.
        message:
          type: string
          description: Человекочитаемое описание ошибки
          example: "Missing required payload field: call_id"
