openapi: 3.1.0
info:
  title: API de Revendedores Covaly
  version: 1.0.0
  description: |
    API central para consultar y gestionar tiendas de un revendedor.
    Las acciones que consumen créditos crean solicitudes pendientes: no se ejecutan
    hasta que una persona las confirma en Covaly.
servers:
  - url: https://covaly.store/api/v1/reseller
    description: API de producción
security:
  - resellerToken: []
tags:
  - name: Cuenta
  - name: Tiendas
  - name: Solicitudes
paths:
  /me:
    get:
      tags: [Cuenta]
      summary: Verifica el token y los permisos
      operationId: getResellerMe
      responses:
        "200":
          description: Cuenta y scopes del token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /stores:
    get:
      tags: [Tiendas]
      summary: Lista las tiendas de la cuenta
      operationId: listResellerStores
      description: Requiere el scope `stores:read`. Solo devuelve tiendas del revendedor autenticado.
      responses:
        "200":
          description: Tiendas de la cuenta
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StoreListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags: [Tiendas]
      summary: Solicita crear una tienda
      operationId: requestResellerStore
      description: |
        Requiere `stores:create`. Crea una solicitud pendiente y responde `202`;
        no aprovisiona la tienda ni consume créditos hasta la confirmación humana.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateStoreRequest"
            example:
              store_name: Mi tienda
              slug: mi-tienda
              owner_email: dueno@ejemplo.com
      responses:
        "202":
          description: Solicitud pendiente de confirmación
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionRequestEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
  /stores/{application}:
    parameters:
      - $ref: "#/components/parameters/ApplicationId"
    get:
      tags: [Tiendas]
      summary: Consulta una tienda
      operationId: getResellerStore
      description: Requiere `stores:read`. Las tiendas de otras cuentas responden `404`.
      responses:
        "200":
          description: Tienda encontrada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StoreResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /stores/{application}/extend:
    parameters:
      - $ref: "#/components/parameters/ApplicationId"
    post:
      tags: [Tiendas]
      summary: Solicita extender el acceso de una tienda
      operationId: requestStoreExtension
      description: |
        Requiere `stores:extend`. Acepta de 1 a 12 meses y crea una solicitud
        pendiente; los créditos solo se descuentan después de confirmar en Covaly.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExtendStoreRequest"
            example:
              months: 1
              idempotency_key: extend-2026-001
      responses:
        "202":
          description: Solicitud pendiente de confirmación
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionRequestEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
  /stores/{application}/catalog-token:
    parameters:
      - $ref: "#/components/parameters/ApplicationId"
    post:
      tags: [Tiendas]
      summary: Emite o rota el token de catálogo
      operationId: issueStoreCatalogToken
      description: |
        Requiere `catalog-token:manage` y una tienda aprovisionada. La respuesta
        incluye el token plano, que solo se entrega en esta operación; guárdalo de forma segura.
      responses:
        "201":
          description: Token emitido
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CatalogTokenResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "502":
          $ref: "#/components/responses/UpstreamFailure"
    delete:
      tags: [Tiendas]
      summary: Revoca el token de catálogo
      operationId: revokeStoreCatalogToken
      description: Requiere `catalog-token:manage` y una tienda aprovisionada.
      responses:
        "204":
          description: Token revocado; respuesta sin cuerpo
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "502":
          $ref: "#/components/responses/UpstreamFailure"
  /action-requests:
    get:
      tags: [Solicitudes]
      summary: Lista las solicitudes de acción
      operationId: listActionRequests
      description: Requiere `stores:read`. Permite consultar el resultado de solicitudes de creación o extensión.
      responses:
        "200":
          description: Solicitudes de la cuenta, de más reciente a más antigua
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionRequestListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /action-requests/{actionRequest}:
    parameters:
      - name: actionRequest
        in: path
        required: true
        description: ID de la solicitud
        schema:
          type: integer
          minimum: 1
    get:
      tags: [Solicitudes]
      summary: Consulta una solicitud
      operationId: getActionRequest
      description: Requiere `stores:read`. Una solicitud de otra cuenta responde `404`.
      responses:
        "200":
          description: Estado de la solicitud
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionRequestSingleResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
components:
  securitySchemes:
    resellerToken:
      type: http
      scheme: bearer
      bearerFormat: rsl_ token
      description: Token central de revendedor.
  parameters:
    ApplicationId:
      name: application
      in: path
      required: true
      description: ID numérico de la tienda en Covaly
      schema:
        type: integer
        minimum: 1
  schemas:
    MeResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [reseller, token]
          properties:
            reseller:
              type: object
              required: [id, name, email]
              properties:
                id: { type: integer }
                name: { type: string }
                email: { type: string, format: email }
            token:
              type: object
              required: [scopes, expires_at]
              properties:
                scopes:
                  type: array
                  items: { type: string }
                  example: [stores:read, stores:create]
                expires_at:
                  type: [string, "null"]
                  format: date-time
    CreateStoreRequest:
      type: object
      additionalProperties: false
      required: [store_name, slug]
      properties:
        store_name: { type: string, maxLength: 255 }
        slug: { type: string, maxLength: 100, description: Subdominio solicitado; debe estar disponible }
        owner_email: { type: [string, "null"], format: email, maxLength: 255 }
    ExtendStoreRequest:
      type: object
      additionalProperties: false
      required: [months, idempotency_key]
      properties:
        months: { type: integer, minimum: 1, maximum: 12 }
        idempotency_key: { type: string, maxLength: 120 }
    Store:
      type: object
      required: [id, store_name, slug, domain, admin_url, status, pause_reason, provisioned_at, access_expires_at, created_at]
      properties:
        id: { type: integer }
        store_name: { type: string }
        slug: { type: string }
        domain: { type: string }
        admin_url: { type: string, format: uri }
        status: { type: string }
        pause_reason: { type: [string, "null"] }
        provisioned_at: { type: [string, "null"], format: date-time }
        access_expires_at: { type: [string, "null"], format: date-time }
        created_at: { type: [string, "null"], format: date-time }
    StoreResponse:
      type: object
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/Store" }
    StoreListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Store" }
    ActionRequest:
      type: object
      required: [id, type, status, payload, error_message, expires_at, confirmed_at, created_at]
      properties:
        id: { type: integer }
        type: { type: string, enum: [create_store, extend_access] }
        status: { type: string, enum: [pending, approved, rejected, expired, failed] }
        payload:
          type: object
          additionalProperties: true
          description: Datos de la solicitud; su estructura depende del campo type.
        result:
          type: object
          required: [type, id]
          properties:
            type: { type: string }
            id: { type: integer }
          description: Solo aparece si la solicitud produjo un recurso.
        error_message: { type: [string, "null"] }
        expires_at: { type: [string, "null"], format: date-time }
        confirmed_at: { type: [string, "null"], format: date-time }
        created_at: { type: [string, "null"], format: date-time }
    ActionRequestEnvelope:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/ActionRequest" }
        meta:
          type: object
          required: [message]
          properties:
            message: { type: string }
    ActionRequestSingleResponse:
      type: object
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/ActionRequest" }
    ActionRequestListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/ActionRequest" }
    CatalogTokenResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [api_token, token_id, name, abilities, expires_at]
          properties:
            api_token: { type: string, description: Token plano; se muestra una sola vez }
            token_id: { type: integer }
            name: { type: string }
            abilities:
              type: array
              items: { type: string }
            expires_at: { type: [string, "null"], format: date-time }
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            details: { type: object, additionalProperties: true }
  responses:
    Unauthorized:
      description: Token ausente, inválido o expirado
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Forbidden:
      description: Token sin el scope necesario
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    NotFound:
      description: Recurso inexistente o que pertenece a otra cuenta
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Conflict:
      description: Estado incompatible; por ejemplo, tienda aún no aprovisionada
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    ValidationFailed:
      description: Solicitud inválida
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    RateLimited:
      description: Límite de solicitudes excedido
      headers:
        Retry-After:
          schema: { type: integer }
          description: Segundos sugeridos antes de reintentar.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    UpstreamFailure:
      description: Error al solicitar o revocar el token en la tienda destino
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
