openapi: 3.0.3
info:
  title: API Vidamos — cumplimiento RDA e RIPS
  version: "1.0"
  description: |
    **Disponibilidad:** cualquier ruta puede responder `503` con cuerpo
    `{ "error", "codigo": "bd-arrancando", "reintentarEnSegundos" }` y cabecera `Retry-After`
    mientras la base de datos despierta tras un periodo de inactividad (±20 s). Reintente
    pasado ese tiempo; ninguna operación se ejecutó.

    API REST del motor de cumplimiento de Vidamos para prestadores y software de salud
    en Colombia:

    - **RDA** (Resolución 1888/2025, mod. 1799/2026): generación, validación y transmisión
      del Resumen Digital de Atención a la plataforma IHCE del Ministerio de Salud, con
      número VIDA y trazabilidad completa.
    - **RIPS** (Resolución 948/2026): validación y radicación del RIPS como soporte de la
      FEV contra el Mecanismo Único de Validación (MUV), con CUV y trazabilidad.

    **Integración 100% headless**: toda la operación se realiza desde el código del
    sistema del cliente. Los errores llegan **accionables y en español** (dónde está el
    problema en términos de negocio, qué está mal y cómo corregirlo) — ver el esquema
    `ErrorAccionable`.

    La autenticación es por API key de la entidad (header `X-Api-Key`), emitida durante
    el onboarding. Las credenciales ante el Ministerio (Entra ID para IHCE, SISPRO para
    el MUV) son de cada entidad y se configuran una sola vez.
  contact:
    name: Vidamos
    email: contacto@vidamos.co
    url: https://vidamos.co
servers:
  - url: https://api.vidamos.co
    description: Producción (el host definitivo se entrega en el onboarding)
security:
  - ApiKey: []
  - UsuarioCognito: []
tags:
  - name: RIPS
    description: Validación y radicación RIPS→MUV (Res. 948/2026)
  - name: RDA
    description: Generación, validación y transmisión RDA→IHCE (Res. 1888/2025)
  - name: Estado
    description: Salud del servicio

paths:
  /rips/validar:
    post:
      tags: [RIPS]
      summary: Validar un RIPS sin transmitir (dry-run)
      description: |
        Ejecuta la puerta local de validación (reglas del Documento Técnico 1 v003 +
        catálogos oficiales) SIN tocar el MUV ni persistir nada. Ideal para integrar en
        el flujo del cliente ANTES de radicar: se corrige hasta que `valid` sea `true`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rips]
              properties:
                rips:
                  description: El RIPS en la estructura JSON del DocTec1 (§6).
                  type: object
      responses:
        "200":
          description: Resultado de la validación con errores explicados.
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                    description: true cuando no hay errores bloqueantes (los avisos no bloquean).
                  issues:
                    type: array
                    description: Forma técnica de cada hallazgo (regla oficial + ruta).
                    items: { $ref: "#/components/schemas/RipsIssue" }
                  errores:
                    type: array
                    description: Los mismos hallazgos, explicados para humanos.
                    items: { $ref: "#/components/schemas/ErrorAccionable" }
        "400": { $ref: "#/components/responses/CuerpoInvalido" }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /rips:
    post:
      tags: [RIPS]
      summary: Radicar un RIPS ante el MUV (obtiene el CUV)
      description: |
        Valida localmente, transmite al Mecanismo Único de Validación con las credenciales
        SISPRO de la entidad y persiste la evidencia del intento. La operación del MUV se
        elige sola según `rips.tipoNota`: factura (`null`), NC, ND, NA (nota de ajuste,
        sin adjunto) o RS (RIPS sin factura, sin adjunto).

        La puerta local revisa dos cosas antes de gastar el viaje al MUV: el RIPS contra el
        Documento Técnico 1 y el XML de la factura contra el Documento Técnico 2 (contenedor
        AttachedDocument con factura y ApplicationResponse embebidos, apartados obligatorios,
        extensión del sector salud, pagos anticipados, periodo de facturación y el cruce
        PFP001: el número de la factura del XML debe ser idéntico a `rips.numFactura`). Los
        rechazos salen como `rechazado-local` con los mismos códigos del MUV (FED*, VFE*,
        PFE*, PFP001, RVG018…) y su `comoCorregir`; las notificaciones del DocTec2 las emite
        el propio MUV al radicar.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rips]
              properties:
                rips:
                  type: object
                  description: El RIPS en la estructura JSON del DocTec1.
                xmlFevFile:
                  type: string
                  description: >
                    AttachedDocument de la FEV en base64 (el contenedor que emite el proveedor
                    tecnológico DIAN con la factura firmada y la respuesta de validación
                    embebidas; una factura suelta se rechaza con PFE001). Obligatorio con
                    factura y notas NC/ND; se omite con tipoNota NA o RS (el manual exige
                    adjunto vacío).
      responses:
        "200":
          description: >
            Idempotencia: la factura (mismo `numFactura` y `numDocumentoIdObligado`) ya fue
            radicada por esta entidad desde Vidamos. Se devuelve la radicación existente con
            `repetida: true`, sin volver al MUV ni crear evidencia nueva (una factura tiene un
            solo CUV). Aplica a facturas; las notas NC/ND/NA/RS siempre van al MUV.
        "201":
          description: Radicado — el MUV emitió CUV.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  estado: { type: string, enum: [radicado] }
                  cuv:
                    type: string
                    description: Código Único de Validación (96 hex) — el comprobante ante el pagador.
                  avisos:
                    type: array
                    description: >
                      Notificaciones de gradualidad (§1.6) explicadas: hoy pasan, serán
                      rechazo cuando el Ministerio lo determine — corregir desde ya.
                    items: { $ref: "#/components/schemas/ErrorAccionable" }
        "422":
          description: >
            Rechazado. `estado` distingue el origen: `rechazado-local` (la puerta local lo
            atrapó — el MUV nunca lo vio) o `rechazado` (veredicto del MUV). En ambos
            casos `errores` trae la lista accionable y el intento queda como evidencia.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  estado: { type: string, enum: [rechazado-local, rechazado] }
                  errores:
                    type: array
                    items: { $ref: "#/components/schemas/ErrorAccionable" }
        "400": { $ref: "#/components/responses/CuerpoInvalido" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "502": { description: "El MUV no está disponible (no hubo radicación — reintentar)." }
        "503": { description: "Transmisión RIPS o credenciales SISPRO no configuradas para la entidad." }
    get:
      tags: [RIPS]
      summary: Listar radicaciones de la entidad
      description: Últimas 100 radicaciones (cada intento es una fila — evidencia append-only).
      responses:
        "200":
          description: Lista con conteos de hallazgos por fila.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/Radicacion" }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /rips/{id}:
    get:
      tags: [RIPS]
      summary: Detalle de una radicación con sus errores accionables
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: La radicación completa; `errores` siempre viene explicado.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Radicacion"
                  - type: object
                    properties:
                      errores:
                        type: array
                        items: { $ref: "#/components/schemas/ErrorAccionable" }
        "404": { description: "No existe (o pertenece a otra entidad — el aislamiento es total)." }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /rips/{id}/cuv:
    get:
      tags: [RIPS]
      summary: Verificar el CUV de una radicación ante el MUV (lo que consulta el pagador)
      description: |
        Consulta `ConsultarCUV` del Mecanismo Único de Validación (manual API-Docker v4.3
        §8.14) con el obligado y el número de documento de la radicación, y compara el CUV
        que devuelve el Ministerio con el que Vidamos guardó. `verificado` es `true` solo si
        el MUV conoce el documento y el CUV coincide. La operación no requiere credenciales
        SISPRO: es la misma consulta que hace la entidad responsable de pago.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Resultado de la verificación.
          content:
            application/json:
              schema:
                type: object
                required: [id, numFactura, cuv, verificado, registro, consultadoEn]
                properties:
                  id: { type: string, format: uuid }
                  numFactura: { type: string }
                  cuv: { type: string, description: El CUV guardado al radicar. }
                  verificado:
                    type: boolean
                    description: El MUV conoce el documento y su CUV es idéntico al guardado.
                  registro:
                    nullable: true
                    description: >
                      Registro del §8.14 en camelCase (procesoId, cuv, numeroDocumento,
                      numDocumentoIdObligado, fechaValidacion, esValido, tipoDocumento,
                      fechaEmision, totalFactura, cantidadUsuarios, codigoPrestador,
                      modalidadPago, numDocumentoReferenciado, resultadosValidacion,
                      cantidadTotalAtenciones). `null` cuando el MUV no conoce el documento.
                    type: object
                  consultadoEn: { type: string, format: date-time }
        "404": { description: "No existe (o pertenece a otra entidad — el aislamiento es total)." }
        "409": { description: "La radicación no tiene CUV (fue rechazada): nada que verificar." }
        "502": { description: "El MUV no respondió a la consulta." }
        "503": { description: "Consulta al MUV no configurada en este ambiente." }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /rda/{id}/ihce:
    get:
      tags: [RDA]
      summary: Recuperar el RDA propio tal como quedó registrado en IHCE
      description: |
        Consulta `GET /Composition/{id}/$document` del gateway (manual de operaciones v1.4
        §5.5 op. 10) con el id de Composition que IHCE devolvió al aceptar el RDA, y reenvía
        el Bundle completo al tenant que lo transmitió. Vidamos no persiste ni registra el
        contenido clínico; la consulta queda en la auditoría del envío. Solo aplica a RDA
        `aceptado`.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: El Bundle FHIR con la Composition y sus recursos, como lo entrega IHCE.
          content:
            application/fhir+json:
              schema: { type: object }
        "404": { description: "No existe, pertenece a otra entidad, o IHCE no encontró el documento." }
        "409": { description: "El RDA no está aceptado (IHCE solo conserva los aceptados) o no tiene id de Composition." }
        "502": { description: "IHCE no respondió o rechazó la consulta." }
        "503": { description: "Credenciales IHCE no configuradas para este tenant." }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /rda/consultas:
    post:
      tags: [RDA]
      summary: Listar los RDA de un paciente registrados en IHCE (consulta del profesional autorizado)
      description: |
        Consulta `$consultar-rda-paciente` (RDA de paciente) o `$consultar-rda-encuentros-clinicos`
        (urgencias, hospitalización, consulta externa) del gateway, manual de operaciones v1.4
        §5.5 op. 7 y 8. El Ministerio exige y audita quién consulta (§5.4 n.º 9): `humanuser`
        es la identificación de la persona, normalmente el profesional de la salud, y es
        obligatoria. Vidamos la deja en su propia auditoría como actor y no persiste el número
        del paciente. La respuesta se reduce a metadatos (id de Composition, VIDA, fecha, tipo);
        el documento completo se pide con `GET /rda/{id}/ihce` cuando es propio.
        Paginación token-based: si `siguiente` no es null, repita la llamada con ese valor en
        `cursor`; un cursor vencido responde 410.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [paciente, humanuser]
              properties:
                alcance:
                  type: string
                  enum: [paciente, encuentros]
                  default: paciente
                paciente:
                  type: object
                  required: [tipo, numero]
                  properties:
                    tipo: { type: string, example: CC }
                    numero: { type: string, example: "000000099" }
                humanuser:
                  type: object
                  required: [tipo, numero]
                  description: Persona que realiza la consulta (profesional de la salud). Obligatoria y auditada.
                  properties:
                    tipo: { type: string, example: CC }
                    numero: { type: string, example: "111111199" }
                cursor:
                  type: string
                  description: El valor de `siguiente` de la página anterior.
      responses:
        "200":
          description: Página de resultados.
          content:
            application/json:
              schema:
                type: object
                required: [alcance, items, siguiente]
                properties:
                  alcance: { type: string, enum: [paciente, encuentros] }
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        compositionId: { type: string }
                        vida: { type: string, nullable: true }
                        fecha: { type: string, nullable: true }
                        tipo: { type: string, nullable: true }
                        paciente: { type: string, nullable: true, description: Referencia FHIR al Patient en IHCE. }
                  siguiente: { type: string, nullable: true, description: Cursor para la página siguiente; null si no hay más. }
                  total: { type: integer, nullable: true }
        "400": { description: "Faltan paciente o humanuser, o tienen forma inválida (detalles en español)." }
        "410": { description: "El cursor de paginación venció en IHCE: repita desde la primera página." }
        "502": { description: "IHCE no respondió o rechazó la consulta." }
        "503": { description: "Credenciales IHCE no configuradas para este tenant." }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /rda/validar:
    post:
      tags: [RDA]
      summary: Validar un Bundle RDA sin transmitir (linter estructural)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bundle]
              properties:
                bundle: { type: object, description: Bundle FHIR R4 del RDA. }
      responses:
        "200":
          description: Issues del linter + errores explicados.
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid: { type: boolean }
                  issues: { type: array, items: { type: object } }
                  errores:
                    type: array
                    items: { $ref: "#/components/schemas/ErrorAccionable" }
        "400": { $ref: "#/components/responses/CuerpoInvalido" }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /rda/validar-oficial:
    post:
      tags: [RDA]
      summary: Validar un Bundle contra el validador oficial HL7 (conformidad de perfiles)
      description: >
        Dry-run contra el validador oficial del estándar con los paquetes
        `minsalud.fhir.co.rda` y FHIR Core CO — la vara de conformidad del Ministerio, más
        estricta que el linter de `/rda/validar`. Es pesado (arranca una JVM): pensado para
        diagnóstico y certificación, no para cada transmisión. Responde 503 si el ambiente no
        tiene validador configurado.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bundle]
              properties:
                bundle: { type: object, description: Bundle FHIR R4 del RDA. }
      responses:
        "200":
          description: Hallazgos del validador oficial, con conteo por severidad.
          content:
            application/json:
              schema:
                type: object
                properties:
                  porSeveridad:
                    type: object
                    additionalProperties: { type: integer }
                    description: Conteo por severidad (error, warning, information, fatal).
                  issues:
                    type: array
                    items:
                      type: object
                      properties:
                        severity: { type: string }
                        message: { type: string }
                        location: { type: string }
                  errores:
                    type: array
                    items: { $ref: "#/components/schemas/ErrorAccionable" }
        "400": { $ref: "#/components/responses/CuerpoInvalido" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "503":
          description: Este ambiente no tiene validador oficial configurado.

  /rda/desde-datos:
    post:
      tags: [RDA]
      summary: Transmitir un RDA desde datos planos (sin saber FHIR)
      description: |
        La promesa del producto: el cliente envía el modelo intermedio en español
        (paciente, autor, prestador, atención, diagnósticos…) y la plataforma construye
        el Bundle conforme a los perfiles oficiales, lo valida y lo transmite a IHCE.
        Si faltan datos que el perfil oficial exige, responde 400 con la lista campo por
        campo en español.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tipo, datos]
              properties:
                tipo:
                  type: string
                  enum: [paciente, consulta-externa, hospitalizacion, urgencias]
                datos:
                  type: object
                  description: Modelo intermedio (documentación detallada en el onboarding).
      responses:
        "201": { description: "Aceptado por IHCE — la respuesta incluye el número VIDA." }
        "202": { description: "En proceso (reintento automático en curso)." }
        "422": { description: "Rechazado — motivo y errores accionables incluidos." }
        "400":
          description: Datos incompletos — `detalles` lista cada campo faltante en español.
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /rda:
    post:
      tags: [RDA]
      summary: Transmitir un Bundle RDA ya construido
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tipo, bundle]
              properties:
                tipo:
                  type: string
                  enum: [paciente, consulta-externa, hospitalizacion, urgencias]
                bundle: { type: object }
      responses:
        "201": { description: "Aceptado — incluye número VIDA." }
        "202": { description: "En proceso." }
        "422": { description: "Rechazado — motivo incluido." }
        "401": { $ref: "#/components/responses/NoAutenticado" }
    get:
      tags: [RDA]
      summary: Listar transmisiones RDA
      parameters:
        - name: estado
          in: query
          schema:
            type: string
            enum: [generado, validado, enviado, aceptado, rechazado]
      responses:
        "200": { description: "Lista paginada (limit/offset) con total." }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /rda/{id}:
    get:
      tags: [RDA]
      summary: Detalle de una transmisión RDA (errores accionables incluidos)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: "La transmisión; si fue rechazada, `errores` viene explicado." }
        "404": { description: "No existe o pertenece a otra entidad." }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /rda/{id}/eventos:
    get:
      tags: [RDA]
      summary: Trazabilidad completa de una transmisión (auditoría)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: "Eventos append-only del envío (el trail que pide la Supersalud)." }
        "404": { description: "No existe o pertenece a otra entidad." }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /resumen:
    get:
      tags: [RDA]
      summary: Resumen de cumplimiento (conteos por estado y tipo)
      responses:
        "200": { description: "porEstado y porTipo." }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /sesion:
    get:
      tags: [Estado]
      summary: Con qué entidad, papel, módulos y permisos entra la credencial
      description: |
        Devuelve el tenant (slug, nombre, tipoEntidad, módulos contratados), la identidad
        (`api-key`, o `usuario` con su rol) y los permisos efectivos: los del rol cuyo módulo
        está contratado. El panel construye la navegación desde esta respuesta. Un módulo no
        contratado responde `403` con `codigo`/`modulo` en cualquiera de sus rutas.
      responses:
        "200": { description: "tenant, identidad y permisos." }
        "401": { $ref: "#/components/responses/NoAutenticado" }

  /health:
    get:
      tags: [Estado]
      security: []
      summary: Liveness
      responses:
        "200": { description: ok }

  /ready:
    get:
      tags: [Estado]
      security: []
      summary: Readiness (estado real de cada dependencia)
      responses:
        "200": { description: ok }
        "503": { description: degradado }

components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: >
        API key de la entidad (formato `rda_…`), emitida en el onboarding. Autentica al
        SISTEMA del prestador (anexo Res. 1888/2025, 6.2.a nivel i) con todos los permisos.
    UsuarioCognito:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        Token de acceso de Cognito de una PERSONA del prestador (nivel ii). El grupo
        `{slug}:{rol}` del token identifica la entidad y el rol (owner, operador, auditor):
        los permisos llevan el módulo como prefijo: `rda:enviar` (transmitir RDA) y
        `rips:enviar` (radicar RIPS) los tienen owner y operador; `rda:leer` y `rips:leer`
        (listar, detalle, validar en seco, consultar IHCE, verificar CUV) los tienen los tres
        roles.
        `X-Tenant: <slug>` elige. Con rol insuficiente la respuesta es 403.
  responses:
    NoAutenticado:
      description: "Credencial inválida o ausente (X-Api-Key o Authorization: Bearer)."
    CuerpoInvalido:
      description: Cuerpo inválido — `error` (y `detalles` cuando aplica) explican qué falta.
  schemas:
    ErrorAccionable:
      type: object
      description: >
        Un hallazgo explicado para humanos: DÓNDE está el problema en términos de negocio,
        QUÉ está mal y CÓMO corregirlo. Diseñado para que el personal operativo resuelva
        sin escalar a soporte.
      properties:
        codigo:
          type: string
          description: Código citable de la regla oficial (RVG03, RVC019, T03, RDA05…).
        severidad:
          type: string
          enum: [rechazo, aviso-gradualidad, aviso-conformidad, informativo]
          description: >
            `rechazo` = impide la operación, el documento no llega al Ministerio.
            `aviso-gradualidad` (RIPS) = hoy pasa pero será rechazo cuando el Ministerio lo
            determine (§1.6) — la radicación SÍ quedó en firme.
            `aviso-conformidad` (RDA) = el gateway lo acepta y devuelve VIDA, pero incumple
            los perfiles `minsalud.fhir.co.rda` del validador oficial — el envío NO se bloquea.
        fuente:
          type: string
          enum: [linter-local, muv, linter-rda, ihce, validador-hl7]
        ubicacion:
          type: object
          properties:
            factura: { type: string }
            usuarioIndex: { type: integer, description: "Base 0." }
            documentoUsuario:
              type: string
              description: "Identificación del paciente (ej. CC 1234567) — dato personal: no registrar en logs."
            tipoServicio: { type: string }
            servicioIndex: { type: integer }
            campo: { type: string }
            campoNombre: { type: string, description: "Nombre de negocio del campo." }
            descripcion:
              type: string
              description: 'Frase lista para mostrar: "Factura FE123 · Paciente CC 1234567 · Consulta 1 · Valor del servicio".'
        titulo: { type: string, description: Qué está mal, en lenguaje llano. }
        explicacion: { type: string }
        comoCorregir: { type: string, description: Acción concreta en el sistema origen. }
        mensajeOriginal: { type: string, description: Detalle técnico para soporte. }
    RipsIssue:
      type: object
      properties:
        severity: { type: string, enum: [error, warning] }
        regla: { type: string, description: Regla oficial del DocTec1. }
        path: { type: string, example: "usuarios[0].servicios.consultas[1].vrServicio" }
        message: { type: string }
    Radicacion:
      type: object
      properties:
        id: { type: string, format: uuid }
        numFactura: { type: string }
        estado: { type: string, enum: [radicado, rechazado] }
        cuv: { type: string, nullable: true }
        procesoId: { type: integer, nullable: true }
        fechaRadicacion: { type: string, nullable: true }
        motivo: { type: string, nullable: true }
        nErrores: { type: integer }
        nAvisos: { type: integer }
        createdAt: { type: string }
