openapi: 3.0.3

info:
  title: API de Consulta de Remesas
  version: "1.0"
  description: |
    Esta API existe para una sola cosa: que el agente de servicio al cliente de
    Red Chapina pueda decirle a una persona en qué va su remesa, sin exponer
    nada que le sirva a un tercero.

    Devuelve **estado y fechas**. No devuelve el nombre del beneficiario, ni el
    monto, ni el punto de cobro. Esa decisión es deliberada: entre menos
    información entregue este endpoint, menos daño causa el día que alguien
    consiga una clave ajena.

    ## Cómo se identifica a quien pregunta

    Hay dos caminos, y el cliente nunca elige cuál: lo decide el agente según
    desde dónde escribe.

    1. **Teléfono reconocido.** Si escribe desde el mismo número con el que se
       registró la remesa, basta la clave. El número de WhatsApp funciona como
       segundo dato y no cuesta nada pedirlo, porque ya viene en el mensaje.
    2. **Teléfono desconocido.** Si escribe desde otro número, hacen falta la
       clave **y** el documento de identidad del remitente.

    Nunca basta un solo dato. Una clave suelta la puede ver cualquiera sobre un
    mostrador; un DPI guatemalteco tampoco es secreto. Juntos sí funcionan.

    ## Límite de intentos

    Cinco intentos fallidos por número de teléfono en una hora. Al sexto, la
    API responde `429` durante sesenta minutos. Sin ese límite, cualquiera con
    un DPI podría probar claves hasta acertar.

    ## Qué hace el agente con el documento

    Lo usa para validar y lo descarta. No lo guarda en su memoria de
    conversación ni lo repite en sus respuestas. El dato queda en el chat del
    cliente y en los servidores de Meta, y eso no lo controlamos, pero del lado
    nuestro no se almacena.
  contact:
    name: Dpto. Comercial — Red Chapina
    email: oscar.jimenez@tranzmit.net

servers:
  - url: https://api-consulta-remesas.pages.dev/v1
    description: Simulador. Responde con datos falsos y la forma exacta de la respuesta real.
  - url: https://{host}/v1
    description: Producción. La define el equipo de tecnología de Red Chapina.
    variables:
      host:
        default: por-definir.redchapina.com

tags:
  - name: Consulta
    description: Estado de una remesa.
  - name: Salud
    description: Para monitoreo.

paths:
  /remesas/consulta:
    post:
      tags: [Consulta]
      summary: Consulta el estado de una remesa
      description: |
        Va por POST y no por GET a propósito: la clave y el documento no deben
        viajar en la URL, porque las URLs quedan escritas en los registros del
        servidor y en los historiales de los proxys intermedios.
      operationId: consultarRemesa
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConsultaRequest'
            examples:
              telefonoReconocido:
                summary: Escribe desde el teléfono registrado
                value:
                  clave: "8451207733"
                  telefono: "+50255512345"
              telefonoDesconocido:
                summary: Escribe desde otro número
                value:
                  clave: "8451207733"
                  telefono: "+50244498877"
                  documento:
                    tipo: DPI
                    numero: "1999100060101"
      responses:
        '200':
          description: Los datos coinciden. Se devuelve el estado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsultaResponse'
              examples:
                disponible:
                  summary: Lista para cobrar
                  value:
                    estado: DISPONIBLE
                    fechaEnvio: "2026-10-26T14:32:11Z"
                    fechaPago: null
                    fechaActualizacion: "2026-10-26T14:33:02Z"
                pagada:
                  summary: Ya la cobraron
                  value:
                    estado: PAGADA
                    fechaEnvio: "2026-10-20T09:11:40Z"
                    fechaPago: "2026-10-21T16:45:03Z"
                    fechaActualizacion: "2026-10-21T16:45:03Z"
                anulada:
                  summary: Anulada
                  value:
                    estado: ANULADA
                    fechaEnvio: "2026-10-18T11:02:55Z"
                    fechaPago: null
                    fechaActualizacion: "2026-10-19T08:20:14Z"
                    motivo: DATOS_DEL_BENEFICIARIO
        '400':
          description: Falta un campo o viene mal formado.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example:
                error: SOLICITUD_INVALIDA
                mensaje: "Hace falta la clave de la remesa."
        '401':
          description: |
            Los datos no coinciden, o hacía falta el documento y no vino.
            **Deliberadamente no distingue** entre «esa clave no existe» y
            «la clave existe pero el documento no coincide»: decirlo le
            confirmaría a un atacante que acertó la mitad.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                noCoincide:
                  value:
                    error: DATOS_NO_COINCIDEN
                    mensaje: "No encontramos una remesa con esos datos."
                    intentosRestantes: 3
                faltaDocumento:
                  value:
                    error: DOCUMENTO_REQUERIDO
                    mensaje: "Este número no está registrado para esa remesa. Pedir documento."
        '429':
          description: Se agotaron los intentos. Bloqueado una hora.
          headers:
            Retry-After:
              description: Segundos que faltan para poder intentar de nuevo.
              schema: { type: integer, example: 3600 }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example:
                error: DEMASIADOS_INTENTOS
                mensaje: "Demasiados intentos. Volvé a probar en una hora."
                reintentarEn: 3600
        '500':
          description: Error del servidor. No es culpa de quien consulta.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example:
                error: ERROR_INTERNO
                mensaje: "No pudimos consultar en este momento."

  /salud:
    get:
      tags: [Salud]
      summary: Comprueba que la API responde
      operationId: salud
      responses:
        '200':
          description: Viva.
          content:
            application/json:
              schema:
                type: object
                properties:
                  estado: { type: string, example: ok }
                  version: { type: string, example: "1.0" }

components:
  securitySchemes:
    claveDeServicio:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        La llave la usa el agente, no el cliente final. Identifica al agente
        ante la API; no identifica a la persona que pregunta.

  schemas:
    ConsultaRequest:
      type: object
      required: [clave, telefono]
      properties:
        clave:
          type: string
          minLength: 6
          maxLength: 32
          description: La clave o número de referencia de la remesa.
          example: "8451207733"
        telefono:
          type: string
          description: |
            Número desde el que escribe la persona, en formato internacional.
            Lo pone el agente automáticamente, no lo escribe el cliente.
          pattern: '^\+[1-9][0-9]{7,14}$'
          example: "+50255512345"
        documento:
          $ref: '#/components/schemas/Documento'

    Documento:
      type: object
      description: |
        Solo hace falta cuando el teléfono no coincide con el registrado.
      required: [tipo, numero]
      properties:
        tipo:
          type: string
          enum: [DPI, PASAPORTE]
        numero:
          type: string
          minLength: 6
          maxLength: 20
          description: Sin espacios ni guiones.
          example: "1999100060101"

    ConsultaResponse:
      type: object
      required: [estado, fechaEnvio, fechaActualizacion]
      properties:
        estado:
          $ref: '#/components/schemas/Estado'
        fechaEnvio:
          type: string
          format: date-time
          description: Cuándo se registró la remesa.
        fechaPago:
          type: string
          format: date-time
          nullable: true
          description: Cuándo se cobró. Viene en nulo si todavía no se cobra.
        fechaActualizacion:
          type: string
          format: date-time
          description: Último cambio de estado. Sirve para decirle al cliente desde cuándo está así.
        motivo:
          $ref: '#/components/schemas/MotivoAnulacion'

    Estado:
      type: string
      description: |
        Cinco estados y nada más. Si el sistema interno maneja más, se mapean a
        estos: el cliente no necesita el detalle operativo y cada estado extra
        es una respuesta más que el agente tiene que saber explicar.
      enum:
        - EN_PROCESO
        - DISPONIBLE
        - PAGADA
        - ANULADA
        - EN_REVISION
      x-significado:
        EN_PROCESO: Se registró y todavía no está lista para cobrar.
        DISPONIBLE: Lista. El beneficiario ya la puede cobrar.
        PAGADA: Ya se cobró.
        ANULADA: No se va a pagar. Viene con motivo.
        EN_REVISION: Detenida por una revisión. Requiere que alguien del equipo la vea.

    MotivoAnulacion:
      type: string
      nullable: true
      description: Solo viene cuando el estado es ANULADA.
      enum:
        - DATOS_DEL_BENEFICIARIO
        - CUENTA_INVALIDA
        - LIMITE_EXCEDIDO
        - SOLICITUD_DEL_REMITENTE
        - OTRO

    Error:
      type: object
      required: [error, mensaje]
      properties:
        error:
          type: string
          enum:
            - SOLICITUD_INVALIDA
            - DATOS_NO_COINCIDEN
            - DOCUMENTO_REQUERIDO
            - DEMASIADOS_INTENTOS
            - ERROR_INTERNO
        mensaje:
          type: string
          description: Texto para el equipo, no para mostrarle al cliente tal cual.
        intentosRestantes:
          type: integer
          nullable: true
        reintentarEn:
          type: integer
          nullable: true
          description: Segundos.

security:
  - claveDeServicio: []
