openapi: "3.1.0"
info:
  title: MiniApp — Eventos Entrantes
  description: >
    Eventos entrantes del aplicativo externo (MiniApp → Acloud).
    El aplicativo externo envia una solicitud POST al endpoint del hook.
  version: "2.0.0"

webhooks:

  message.new.personal:
    post:
      summary: Nuevo mensaje entrante del usuario
      description: >
        Enviado por el aplicativo externo al recibir un nuevo mensaje del usuario.
        Tambien se usa como confirmacion de inicializacion del chat en respuesta
        al hook saliente chat.init.personal.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.new.personal
                payload:
                  $ref: '#/components/schemas/MessageNewPayload'
            examples:
              message_new:
                summary: Nuevo mensaje
                value:
                  method: message.new.personal
                  payload:
                    external_message_id: msg_123
                    external_user_id: user_456
                    external_chat_id: chat_789
                    text: Hola, quiero preguntar sobre mi pedido
                    send_date: 1710752400
                    user_data:
                      name: Juan Garcia
                      username: juan
                      phone: "+79001234567"
                      email: juan@example.com
                      avatar_url: https://example.com/avatar.jpg
                      public_link: https://example.com/juan
              message_new_with_attachment:
                summary: Nuevo mensaje con archivo adjunto
                value:
                  method: message.new.personal
                  payload:
                    external_message_id: msg_124
                    external_user_id: user_456
                    external_chat_id: chat_789
                    text: Aqui esta la captura de pantalla que pidieron
                    send_date: 1710752500
                    user_data:
                      name: Juan Garcia
                      username: juan
                      phone: "+79001234567"
                      email: juan@example.com
                      avatar_url: https://example.com/avatar.jpg
                      public_link: https://example.com/juan
                    attachments:
                      - id: ext_att_1001
                        url: https://miniapp.example/files/1001
                        filename: screenshot.png
                        size: 204800
                        type: photo
              message_new_with_failed_attachments:
                summary: >
                  Nuevo mensaje con declaracion de entrega parcial de archivos adjuntos:
                  el aplicativo externo entrego algunos archivos e informa a Acloud
                  que algunos no fueron incluidos en el array attachments.
                value:
                  method: message.new.personal
                  payload:
                    external_message_id: msg_125
                    external_user_id: user_456
                    external_chat_id: chat_789
                    text: Enviando foto
                    send_date: 1710752600
                    user_data:
                      name: Juan Garcia
                      username: juan
                      phone: "+79001234567"
                    attachments:
                      - id: ext_att_1002
                        url: https://miniapp.example/files/1002
                        filename: photo_1.jpg
                        size: 524288
                        type: photo
                    has_failed_attachments: true
                    failed_attachments_count: 4
      responses:
        "200":
          description: Evento procesado correctamente
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Error de validacion (formato invalido o campos obligatorios faltantes)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: >
            Bot desactivado o cuenta bloqueada.
            Deje de enviar eventos hasta recibir el hook saliente bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot o cuenta eliminados.
            Elimine la integracion de su lado y deje de enviar eventos.
        "500":
          description: Error interno del servidor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.delete.personal:
    post:
      summary: Mensaje eliminado por el usuario
      description: >
        Enviado por el aplicativo externo cuando el usuario elimina un mensaje.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.delete.personal
                payload:
                  $ref: '#/components/schemas/MessageDeletedPayload'
            examples:
              message_deleted:
                summary: Eliminacion de mensaje
                value:
                  method: message.delete.personal
                  payload:
                    external_message_id: msg_123
                    deleted_date: 1710752400
      responses:
        "200":
          description: Evento procesado correctamente
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Error de validacion (formato invalido o campos obligatorios faltantes)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: >
            Mensaje con el external_message_id especificado no encontrado en el alcance del
            bot dueno del webhook (cross-bot, no existe, o el mensaje es un mensaje saliente
            del gestor y no puede modificarse externamente).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Not found
        "409":
          description: >
            Bot desactivado o cuenta bloqueada.
            Deje de enviar eventos hasta recibir el hook saliente bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot o cuenta eliminados.
            Elimine la integracion de su lado y deje de enviar eventos.
        "500":
          description: Error interno del servidor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.edit.personal:
    post:
      summary: Mensaje editado por el usuario
      description: >
        Enviado por el aplicativo externo cuando el usuario edita un mensaje.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.edit.personal
                payload:
                  $ref: '#/components/schemas/MessageEditedPayload'
            examples:
              message_edited:
                summary: Edicion de mensaje
                value:
                  method: message.edit.personal
                  payload:
                    external_message_id: msg_123
                    edited_date: 1710752400
                    new_text: Texto del mensaje corregido
      responses:
        "200":
          description: Evento procesado correctamente
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Error de validacion (formato invalido o campos obligatorios faltantes)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: >
            Mensaje con el external_message_id especificado no encontrado en el alcance del
            bot dueno del webhook (cross-bot, no existe, o el mensaje es un mensaje saliente
            del gestor y no puede modificarse externamente).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Not found
        "409":
          description: >
            Bot desactivado o cuenta bloqueada.
            Deje de enviar eventos hasta recibir el hook saliente bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot o cuenta eliminados.
            Elimine la integracion de su lado y deje de enviar eventos.
        "500":
          description: Error interno del servidor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  error:
    post:
      summary: Notificacion de error al procesar hook saliente
      description: >
        El aplicativo externo envia este hook si no pudo procesar una solicitud saliente
        de Acloud (por ejemplo, usuario no encontrado en el sistema). El campo event_id
        se devuelve sin cambios desde el campo event_id del payload del hook saliente.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - error
                payload:
                  $ref: '#/components/schemas/ErrorPayload'
            examples:
              error_user_not_found:
                summary: Usuario no encontrado (error message.new.personal)
                value:
                  method: error
                  payload:
                    message: "User not found"
                    event_id: '550e8400-e29b-41d4-a716-446655440000:79000'
              error_chat_init:
                summary: Error de inicializacion de chat (chat.init.personal)
                value:
                  method: error
                  payload:
                    message: "User lookup failed"
                    event_id: '7c9e6679-7425-40de-944b-e07fc1f90ae7:79000'
      responses:
        "200":
          description: Error aceptado y procesado
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Error de validacion (formato invalido o campos obligatorios faltantes)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: >
            Bot desactivado o cuenta bloqueada.
            Deje de enviar eventos hasta recibir el hook saliente bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot o cuenta eliminados.
            Elimine la integracion de su lado y deje de enviar eventos.
        "500":
          description: Error interno del servidor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.completed.personal:
    post:
      summary: Confirmacion de entrega de mensaje saliente
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.completed.personal
                payload:
                  $ref: '#/components/schemas/MessageCompletedPayload'
            examples:
              message_completed:
                summary: Confirmacion de entrega completa de mensaje saliente
                value:
                  method: message.completed.personal
                  payload:
                    inner_message_id: 9001
                    external_message_id: ext_msg_555
              message_completed_partial:
                summary: >
                  Confirmacion de entrega parcial de mensaje con archivos:
                  algunos archivos entregados al cliente, otros no
                value:
                  method: message.completed.personal
                  payload:
                    inner_message_id: 9002
                    external_message_id: ext_msg_556
                    delivered_attachments:
                      - "555"
                    failed_attachments:
                      - "556"
      responses:
        "200":
          description: Evento procesado correctamente
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Error de validacion (formato invalido o campos obligatorios faltantes)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: >
            Mensaje saliente con el inner_message_id especificado no encontrado en el alcance
            del bot dueno del webhook (cross-bot — intento de confirmar entrega de mensaje
            de otro bot).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Not found
        "409":
          description: >
            Bot desactivado o cuenta bloqueada.
            Deje de enviar eventos hasta recibir el hook saliente bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot o cuenta eliminados.
            Elimine la integracion de su lado y deje de enviar eventos.
        "500":
          description: Error interno del servidor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.read.personal:
    post:
      summary: Mensaje leido por el usuario (aceptado como no-op)
      description: >
        Enviado por el aplicativo externo cuando el usuario lee un mensaje.
        Acloud acepta el evento y devuelve 200 OK, pero **el manejo completo de lectura
        aun no esta implementado** (no-op handler) — la semantica real de lectura es trabajo
        futuro. El contrato esta fijado para que el aplicativo externo que envia este hook
        no reciba 5xx.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.read.personal
                payload:
                  $ref: '#/components/schemas/MessageReadPayload'
            examples:
              message_read:
                summary: Mensaje leido
                value:
                  method: message.read.personal
                  payload:
                    message_id: msg_123
                    read_date: 1710752400
                    user_id: user_456
                    chat_id: chat_789
      responses:
        "200":
          description: Evento aceptado (no-op, sin efectos secundarios)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "409":
          description: >
            Bot desactivado o cuenta bloqueada.
            Deje de enviar eventos hasta recibir el hook saliente bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot o cuenta eliminados.
            Elimine la integracion de su lado y deje de enviar eventos.
        "500":
          description: Error interno del servidor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error


components:
  schemas:

    IncomingEvent:
      type: object
      required:
        - method
        - payload
      properties:
        method:
          type: string
          enum:
            - message.new.personal
            - message.completed.personal
            - message.delete.personal
            - message.edit.personal
            - message.read.personal
            - error
          description: Tipo de evento
        payload:
          oneOf:
            - $ref: '#/components/schemas/MessageNewPayload'
            - $ref: '#/components/schemas/MessageCompletedPayload'
            - $ref: '#/components/schemas/MessageDeletedPayload'
            - $ref: '#/components/schemas/MessageEditedPayload'
            - $ref: '#/components/schemas/MessageReadPayload'
            - $ref: '#/components/schemas/ErrorPayload'
          discriminator:
            propertyName: method

    # ─── message.new.personal ─────────────────────────────────────────────────

    MessageNewPayload:
      type: object
      description: Nuevo mensaje entrante de un usuario del aplicativo externo
      required:
        - external_message_id
        - external_user_id
        - external_chat_id
      properties:
        external_message_id:
          type: string
          description: ID unico del mensaje en el sistema externo
        external_user_id:
          type: string
          description: ID unico del usuario en el sistema externo
        external_chat_id:
          type: string
          description: ID unico del chat en el sistema externo
        text:
          type: string
          default: ""
          description: Texto del mensaje
        send_date:
          type: integer
          description: Unix timestamp del momento de envio del mensaje
          example: 1710752400
        user_data:
          $ref: '#/components/schemas/UserData'
        attachments:
          type: array
          default: []
          items:
            $ref: '#/components/schemas/AttachmentObject'
          description: >
            Array de archivos adjuntos al mensaje. Si no se especifica o esta vacio,
            el mensaje consiste solo en texto.
        direction:
          type: ["integer", "null"]
          enum: [0, 1, null]
          default: null
          description: >
            Direccion del mensaje. 0 — entrante (del usuario al gestor),
            1 — saliente (del gestor al usuario, enviado en el sistema externo).
            Si no se especifica — se trata como entrante.
        has_failed_attachments:
          type: ["boolean", "null"]
          default: null
          description: >
            Indicador opcional declarado por el aplicativo externo de que el mensaje original
            del usuario contenia mas archivos adjuntos de los que se pasaron a Acloud en el
            array attachments. La razon puede ser cualquiera del lado del aplicativo externo
            (limite de tamano superado, error de descarga, politica del mensajero, etc.).
            Actualmente Acloud acepta el campo y registra el hecho, pero no realiza procesamiento
            especial ni lo guarda en la BD. El campo esta reservado para implementacion futura —
            simetrico con delivered_attachments / failed_attachments en el hook saliente
            message.completed.personal.
        failed_attachments_count:
          type: ["integer", "null"]
          minimum: 0
          default: null
          description: >
            Numero opcional de archivos que el aplicativo externo no entrego.
            Solo tiene sentido cuando has_failed_attachments=true. Si se envia sin
            has_failed_attachments=true, el valor se ignora y se escribe una advertencia
            en el log de Acloud.

    # ─── message.delete.personal ──────────────────────────────────────────────

    MessageDeletedPayload:
      type: object
      description: Evento de eliminacion de mensaje por el usuario en el sistema externo.
      required:
        - external_message_id
      properties:
        external_message_id:
          type: string
          description: ID del mensaje en el sistema externo
          example: msg_123
        deleted_date:
          type: integer
          description: Unix timestamp del momento de eliminacion del mensaje
          example: 1710752400

    # ─── message.read.personal ────────────────────────────────────────────────

    MessageReadPayload:
      type: object
      description: >
        Evento de lectura de mensaje por el usuario en el sistema externo.
        Acloud acepta el payload, pero en la version actual **no lo procesa**
        (no-op handler). El contrato esta fijado para compatibilidad con el procesamiento
        futuro; todos los campos son opcionales (defaults en DTO).
      properties:
        message_id:
          type: string
          description: ID del mensaje leido en el sistema externo
          example: msg_123
          default: ""
        read_date:
          type: integer
          description: Unix timestamp del momento de lectura
          example: 1710752400
        user_id:
          type: string
          description: ID del usuario que leyo el mensaje
          example: user_456
          default: ""
        chat_id:
          type: string
          description: ID del chat
          example: chat_789
          default: ""

    # ─── message.edit.personal ────────────────────────────────────────────────

    MessageEditedPayload:
      type: object
      description: Evento de edicion de mensaje por el usuario en el sistema externo.
      required:
        - external_message_id
      properties:
        external_message_id:
          type: string
          description: ID del mensaje en el sistema externo
          example: msg_123
        edited_date:
          type: integer
          description: Unix timestamp del momento de edicion del mensaje
          example: 1710752400
        new_text:
          type: string
          description: Nuevo texto del mensaje tras la edicion
          default: ""
          example: Texto del mensaje corregido

    # ─── message.completed.personal ───────────────────────────────────────────

    MessageCompletedPayload:
      type: object
      description: >
        El aplicativo externo confirma la entrega exitosa de un mensaje enviado por nuestro
        sistema. Se usa para vincular inner_message_id (nuestro) con external_message_id (suyo).


        Comportamiento con entrega parcial de archivos: si algunos archivos del array attachments
        del hook saliente fueron entregados al destinatario y otros no, el aplicativo externo
        igualmente envia message.completed.personal e indica el resultado de entrega de cada
        archivo en los campos delivered_attachments y failed_attachments.

        Si todos los archivos fueron entregados — los campos pueden omitirse o enviarse la lista
        completa en delivered_attachments.

        Si ninguno de los archivos pudo entregarse (y la parte de texto tambien es invalida para
        el cliente) — se envia el evento error en lugar de message.completed.personal.
      required:
        - inner_message_id
        - external_message_id
      properties:
        inner_message_id:
          type: integer
          description: ID del mensaje en nuestro sistema (el valor inner_message_id del hook saliente message.new.personal)
          example: 9001
        external_message_id:
          type: string
          description: ID asignado al mensaje en el sistema externo
          example: ext_msg_555
        delivered_attachments:
          type: array
          default: []
          items:
            type: string
          description: >
            Lista de IDs de archivos adjuntos (correspondientes al id del array attachments del
            message.new.personal saliente) que fueron entregados exitosamente al destinatario.
            Campo opcional: puede omitirse si todos los archivos fueron entregados.

            IMPORTANTE: en la version actual Acloud acepta este campo pero no lo usa para logica
            especial — el estado del mensaje se establece en OK (entrega completa). El campo esta
            reservado para el manejo futuro de entrega parcial. El aplicativo externo debe enviarlo
            ahora para no tener que cambiar su lado cuando se agregue el manejo.
          example: ["555", "556"]
        failed_attachments:
          type: array
          default: []
          items:
            type: string
          description: >
            Lista de IDs de archivos adjuntos que no pudieron entregarse al destinatario.
            Campo opcional: puede omitirse si todos los archivos fueron entregados.

            IMPORTANTE: en la version actual Acloud acepta este campo pero no lo usa para logica
            especial — el estado del mensaje se establece en OK incluso con failed_attachments.
            El campo esta reservado para el manejo futuro de entrega parcial. El aplicativo externo
            debe enviarlo ahora para no tener que cambiar su lado cuando se agregue el manejo.
          example: ["557"]

    # ─── Shared types ─────────────────────────────────────────────────────────

    UserData:
      type: object
      description: Datos del usuario del sistema externo
      properties:
        name:
          type: ["string", "null"]
          description: Nombre visible
        username:
          type: ["string", "null"]
          description: Nombre de usuario
        phone:
          type: ["string", "null"]
        email:
          type: ["string", "null"]
        avatar_url:
          type: ["string", "null"]
          description: URL del avatar
        public_link:
          type: ["string", "null"]
          description: Enlace publico al perfil

    AttachmentObject:
      type: object
      description: >
        Metadatos del archivo adjunto en el mensaje entrante. Acloud descargara el archivo
        desde la URL proporcionada y lo almacenara. Tamano maximo permitido — 50 MB
        (52428800 bytes); los archivos mas grandes se rechazan.
      required:
        - id
        - url
        - filename
      properties:
        id:
          type: string
          description: >
            ID unico del archivo adjunto en el sistema externo.
            Se usa para deduplicacion en llamadas repetidas al webhook.
        url:
          type: string
          description: URL desde el que Acloud descarga el archivo.
        filename:
          type: string
          description: Nombre del archivo con extension.
        size:
          type: integer
          default: 0
          description: >
            Tamano del archivo en bytes. No debe superar 50 MB (52428800).
            Acloud verifica el tamano antes y despues de la descarga.
        type:
          type: string
          default: file
          description: >
            Indicacion del tipo de archivo adjunto (photo / file / video / audio).
            Acloud no usa este campo — el tipo real se determina por el mime del archivo descargado.
        metadata:
          type: ["object", "null"]
          description: Metadatos arbitrarios (opcional).

    # ─── error ────────────────────────────────────────────────────────────────

    ErrorPayload:
      type: object
      description: >
        Payload del evento error. El aplicativo externo senala que no pudo procesar
        un hook saliente (message.new.personal o chat.init.personal).
      required:
        - event_id
      properties:
        message:
          type: string
          description: Descripcion del error legible por humanos
          example: "User not found"
        event_id:
          type: string
          description: >
            El valor del campo event_id del payload del hook saliente. Se devuelve sin cambios
            (como cadena opaca). Si el identificador ya no es conocido por Acloud (por ejemplo,
            el TTL expiro), el evento se ignora de forma segura sin efectos secundarios.
          example: '550e8400-e29b-41d4-a716-446655440000:79000'

    # ─── Shared types ─────────────────────────────────────────────────────────

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string

    ConflictResponse:
      type: object
      description: >
        Bot desactivado o cuenta bloqueada. El aplicativo externo debe dejar de enviar
        eventos para este bot hasta recibir el hook saliente bot.activated.
      required:
        - success
        - error
        - message
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          description: Codigo de error
          enum:
            - bot_deactivated
            - account_expired
          example: bot_deactivated
        message:
          type: string
          description: Descripcion legible de la razon
          example: Bot is deactivated
