openapi: 3.1.0
info:
  title: Roustix API
  version: 1.0.0
  description: |
    Contrato oficial Roustix API v1 — fuente machine-readable alineada a MAG v1.0.
    MAG describe la API para personas; OpenAPI describe la misma API para herramientas.
  contact:
    name: Roustix Developer Support
    url: https://developer.roustix.app
  license:
    name: Proprietary
    url: https://roustix.app/terms

servers:
  - url: https://api.roustix.app/api/v1
    description: Producción
  - url: http://127.0.0.1:5000/api/v1
    description: Desarrollo local

security:
  - bearerAuth: []

tags:
  - name: System
    description: Metadatos públicos y seguros de la aplicación
  - name: Authentication
    description: JWT · MAG-02
  - name: Maintenance
    description: Activos · órdenes · mantenimiento · MAG-04
  - name: Inventory
    description: Productos · stock · almacenes
  - name: Purchasing
    description: Compras
  - name: Sales
    description: Ventas
  - name: CRM
    description: Clientes y relaciones
  - name: Admin
    description: Administración del tenant

paths:
  /version:
    get:
      tags: [System]
      summary: Consultar versión de Roustix
      operationId: systemVersion
      security: []
      responses:
        "200":
          description: Versión SemVer y build desplegado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VersionResponse"

  /auth/login:
    post:
      tags: [Authentication]
      summary: Login JWT
      description: Emite token JWT con contexto de tenant (empresa_slug).
      operationId: authLogin
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
      responses:
        "200":
          description: Token emitido
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LoginResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimitExceeded"

  /me:
    get:
      tags: [Authentication]
      summary: Contexto del tenant activo
      operationId: authMe
      responses:
        "200":
          description: Usuario y tenant del JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /maintenance/assets:
    get:
      tags: [Maintenance]
      summary: Listar activos
      operationId: maintenanceAssetsList
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PageSize"
      responses:
        "200":
          description: Lista paginada de activos
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssetListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      tags: [Maintenance]
      summary: Crear activo
      operationId: maintenanceAssetsCreate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssetCreateRequest"
      responses:
        "201":
          description: Activo creado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResourceCreatedResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"

  /maintenance/assets/{asset_id}:
    get:
      tags: [Maintenance]
      summary: Obtener activo
      operationId: maintenanceAssetsGet
      parameters:
        - $ref: "#/components/parameters/AssetId"
      responses:
        "200":
          description: Activo
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssetResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [Maintenance]
      summary: Actualizar activo
      operationId: maintenanceAssetsPatch
      parameters:
        - $ref: "#/components/parameters/AssetId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssetPatchRequest"
      responses:
        "200":
          description: Activo actualizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssetResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"

  /maintenance/work-orders:
    get:
      tags: [Maintenance]
      summary: Listar órdenes de trabajo
      operationId: maintenanceWorkOrdersList
      x-mag-status: planned
      responses:
        "200":
          description: Lista paginada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkOrderListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /inventory/products:
    get:
      tags: [Inventory]
      summary: Listar productos
      operationId: inventoryProductsList
      x-mag-status: planned
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PageSize"
      responses:
        "200":
          description: Lista paginada de productos
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Product"
                  meta:
                    $ref: "#/components/schemas/Pagination"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /maintenance/incidents:
    get:
      tags: [Maintenance]
      summary: Listar incidencias del tenant
      operationId: maintenanceIncidentsList
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PageSize"
        - {name: status, in: query, schema: {type: string}}
        - {name: priority, in: query, schema: {type: string}}
        - {name: asset_id, in: query, schema: {type: integer}}
        - {name: updated_since, in: query, schema: {type: string, format: date-time}}
      responses:
        "200": {description: Incidencias paginadas}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "403": {$ref: "#/components/responses/Forbidden"}
    post:
      tags: [Maintenance]
      summary: Reportar incidencia de forma idempotente
      operationId: maintenanceIncidentsCreate
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/IncidentCreate"}
      responses:
        "201": {description: Incidencia creada o respuesta idempotente}
        "400": {$ref: "#/components/responses/BadRequest"}
        "409": {description: Conflicto de idempotencia}

  /maintenance/incidents/{incident_id}:
    get:
      tags: [Maintenance]
      summary: Consultar incidencia
      operationId: maintenanceIncidentsGet
      parameters:
        - $ref: "#/components/parameters/IncidentId"
      responses:
        "200": {description: Incidencia del tenant}
        "404": {$ref: "#/components/responses/NotFound"}

  /maintenance/work-orders/{work_order_id}:
    get:
      tags: [Maintenance]
      summary: Consultar orden de trabajo
      operationId: maintenanceWorkOrdersGet
      parameters:
        - $ref: "#/components/parameters/WorkOrderId"
      responses:
        "200": {description: Orden de trabajo del tenant}
        "404": {$ref: "#/components/responses/NotFound"}

  /maintenance/assets/{asset_id}/meters:
    get:
      tags: [Maintenance]
      summary: Listar medidores de un activo
      operationId: maintenanceAssetMetersList
      parameters:
        - $ref: "#/components/parameters/AssetId"
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PageSize"
        - {name: active, in: query, schema: {type: boolean}}
        - {name: updated_since, in: query, schema: {type: string, format: date-time}}
      responses:
        "200": {description: Medidores paginados}
        "404": {$ref: "#/components/responses/NotFound"}

  /maintenance/meters/{meter_id}/readings:
    get:
      tags: [Maintenance]
      summary: Listar lecturas de un medidor
      operationId: maintenanceMeterReadingsList
      parameters:
        - $ref: "#/components/parameters/MeterId"
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PageSize"
        - {name: measured_since, in: query, schema: {type: string, format: date-time}}
        - {name: measured_until, in: query, schema: {type: string, format: date-time}}
      responses:
        "200": {description: Lecturas paginadas}
        "404": {$ref: "#/components/responses/NotFound"}
    post:
      tags: [Maintenance]
      summary: Registrar lectura idempotente
      operationId: maintenanceMeterReadingsCreate
      parameters:
        - $ref: "#/components/parameters/MeterId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/MeterReadingCreate"}
      responses:
        "201": {description: Lectura creada o respuesta idempotente}
        "400": {$ref: "#/components/responses/BadRequest"}
        "409": {description: Conflicto de idempotencia}

  /admin/summary:
    get:
      tags: [Admin]
      summary: Resumen administrativo del tenant
      operationId: adminSummary
      responses:
        "200":
          description: Métricas resumidas
          content:
            application/json:
              schema:
                type: object
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /admin/integration-credentials:
    get:
      tags: [Admin]
      summary: Listar credenciales de integración del tenant
      operationId: integrationCredentialsList
      security:
        - cookieAuth: []
      responses:
        "200":
          description: Credenciales sin secretos recuperables
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/IntegrationCredential"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      tags: [Admin]
      summary: Crear credencial de integración
      description: El campo secret aparece únicamente en esta respuesta.
      operationId: integrationCredentialsCreate
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationCredentialCreate"
      responses:
        "201":
          description: Credencial emitida con secreto de visualización única
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"

  /admin/integration-credentials/{credential_id}/rotate:
    post:
      tags: [Admin]
      summary: Rotar credencial
      operationId: integrationCredentialsRotate
      security:
        - cookieAuth: []
      parameters:
        - $ref: "#/components/parameters/CredentialId"
      responses:
        "201":
          description: Nueva credencial y secreto de visualización única
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /admin/integration-credentials/{credential_id}/revoke:
    post:
      tags: [Admin]
      summary: Revocar credencial inmediatamente
      operationId: integrationCredentialsRevoke
      security:
        - cookieAuth: []
      parameters:
        - $ref: "#/components/parameters/CredentialId"
      responses:
        "200":
          description: Credencial revocada
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT or rtx_* API key
      description: JWT de usuario o API key de integración · MAG-02
    cookieAuth:
      type: apiKey
      in: cookie
      name: session
      description: Sesión web administrativa de Roustix

  parameters:
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PageSize:
      name: page_size
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    AssetId:
      name: asset_id
      in: path
      required: true
      schema:
        type: integer
    CredentialId:
      name: credential_id
      in: path
      required: true
      schema:
        type: integer
    IncidentId:
      name: incident_id
      in: path
      required: true
      schema: {type: integer}
    WorkOrderId:
      name: work_order_id
      in: path
      required: true
      schema: {type: integer}
    MeterId:
      name: meter_id
      in: path
      required: true
      schema: {type: integer}
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 120

  schemas:
    LoginRequest:
      type: object
      required: [username, password, empresa_slug]
      properties:
        username:
          type: string
        password:
          type: string
          format: password
        empresa_slug:
          type: string

    LoginResponse:
      type: object
      required: [token]
      properties:
        token:
          type: string
        expires_in:
          type: integer
          example: 86400
        empresa_id:
          type: integer
        empresa_slug:
          type: string
        rol:
          type: string
        username:
          type: string

    MeResponse:
      type: object
      properties:
        user_id:
          type: integer
        empresa_id:
          type: integer
        empresa_slug:
          type: string
        rol:
          type: string
        identity_type:
          type: string
          enum: [session, jwt, api_key]
        credential_id:
          type: integer
        scopes:
          type: array
          items:
            type: string

    IntegrationCredentialCreate:
      type: object
      required: [name, environment, scopes]
      properties:
        name:
          type: string
          maxLength: 120
        environment:
          type: string
          enum: [test, live]
        scopes:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: string
        expires_at:
          type: [string, "null"]
          format: date-time

    IncidentCreate:
      type: object
      required: [title, description, responsible_area, type]
      properties:
        title: {type: string, maxLength: 200}
        description: {type: string}
        asset_id: {type: [integer, "null"]}
        reported_by: {type: string, maxLength: 200}
        reporter_area: {type: string, maxLength: 120}
        responsible_area: {type: string, maxLength: 120}
        location: {type: string, maxLength: 200}
        type:
          type: string
          enum: [mecanica, electrica, hidraulica, seguridad, ambiental, otro]
        priority:
          type: string
          enum: [baja, media, alta, critica]
        equipment_stopped: {type: boolean, default: false}
        event_date: {type: [string, "null"], format: date}
        event_time: {type: [string, "null"], pattern: "^[0-2][0-9]:[0-5][0-9]$"}

    MeterReadingCreate:
      type: object
      required: [value]
      properties:
        value: {type: [number, string]}
        measured_at: {type: string, format: date-time}
        work_order_id: {type: integer}
        notes: {type: string, maxLength: 500}
        anomaly_type:
          type: string
          enum: [reset, replacement, rollover, sequence_adjustment, out_of_range]
        justification: {type: string, maxLength: 500}
        correction_of_id: {type: integer}

    IntegrationCredential:
      type: object
      properties:
        credential_id:
          type: integer
        name:
          type: string
        key_prefix:
          type: string
        environment:
          type: string
          enum: [test, live]
        scopes:
          type: array
          items:
            type: string
        active:
          type: boolean
        expires_at:
          type: [string, "null"]
          format: date-time
        last_used_at:
          type: [string, "null"]
          format: date-time

    User:
      type: object
      properties:
        user_id:
          type: integer
        name:
          type: string
        role:
          type: string

    Asset:
      type: object
      properties:
        asset_id:
          type: integer
        asset_code:
          type: string
        name:
          type: string
        status:
          type: string
          enum: [operational, maintenance, inactive]
        critical:
          type: boolean
        location:
          type: string

    AssetCreateRequest:
      type: object
      required: [asset_code, name]
      properties:
        asset_code:
          type: string
        name:
          type: string
        critical:
          type: boolean
          default: false

    AssetPatchRequest:
      type: object
      properties:
        name:
          type: string
        status:
          type: string
          enum: [operational, maintenance, inactive]
        critical:
          type: boolean

    AssetResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/Asset"

    AssetListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Asset"
        meta:
          type: object
          properties:
            pagination:
              $ref: "#/components/schemas/Pagination"

    WorkOrder:
      type: object
      properties:
        work_order_id:
          type: integer
        asset_id:
          type: integer
        status:
          type: string
        title:
          type: string

    WorkOrderListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/WorkOrder"
        meta:
          type: object
          properties:
            pagination:
              $ref: "#/components/schemas/Pagination"

    Product:
      type: object
      properties:
        product_id:
          type: integer
        sku:
          type: string
        name:
          type: string

    Pagination:
      type: object
      properties:
        page:
          type: integer
        page_size:
          type: integer
        total:
          type: integer

    VersionResponse:
      type: object
      required: [application, version, release]
      properties:
        application:
          type: string
          const: Roustix
        version:
          type: string
          pattern: '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)'
          example: 1.0.0
        release:
          type: string
          example: v1.0.0
        commit:
          type: string
          description: SHA Git abreviado; solo aparece cuando la plataforma lo suministra.
          example: ea86634abc12

    ResourceCreatedResponse:
      type: object
      properties:
        data:
          type: object
          additionalProperties: true
        meta:
          type: object
          properties:
            api_version:
              type: string
              example: v1

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              example: RESOURCE_NOT_FOUND
            message:
              type: string
            details:
              type: object
              additionalProperties: true

  responses:
    BadRequest:
      description: Solicitud inválida
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: INVALID_REQUEST
              message: Solicitud inválida
    Unauthorized:
      description: No autenticado
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: UNAUTHORIZED
              message: Token inválido o expirado
    Forbidden:
      description: Sin permisos
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: FORBIDDEN
              message: Acceso denegado
    NotFound:
      description: Recurso no encontrado
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: RESOURCE_NOT_FOUND
              message: Recurso no encontrado
    ValidationError:
      description: Error de validación
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: VALIDATION_ERROR
              message: Datos inválidos
              details: {}
    RateLimitExceeded:
      description: Límite de solicitudes excedido
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: RATE_LIMIT_EXCEEDED
              message: Se ha excedido el límite de solicitudes.
    InternalError:
      description: Error interno
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: INTERNAL_ERROR
              message: Error interno del servidor
