openapi: 3.1.0
info:
  title: Descuentazos API
  version: 1.0.0
  description: |
    API pública de Descuentazos para publicar ofertas programáticamente.

    Flujo: creá una API key en la app (Perfil → Ajustes → API keys), canjeala
    por un access token corto en el servicio de autenticación, y usá ese token
    contra la API de ofertas.
servers:
  - url: https://api.descuentazos.example
    description: API de ofertas (deals service)
  - url: https://auth.descuentazos.example
    description: Autenticación (users service)

paths:
  /v1/auth/token:
    post:
      summary: Canjear una API key por un access token
      description: Servidor `auth.` — token válido por 1 hora, scope `deals:write`.
      operationId: exchangeApiKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [apiKey]
              properties:
                apiKey:
                  type: string
                  example: dk_live_a1b2c3...
      responses:
        '200':
          description: Token emitido
          content:
            application/json:
              schema:
                type: object
                properties:
                  accessToken: {type: string}
                  expiresIn: {type: integer, example: 3600}
                  tokenType: {type: string, example: api_key}
                  scopes:
                    type: array
                    items: {type: string}
        '401':
          $ref: '#/components/responses/Error'

  /v1/deals:
    post:
      summary: Publicar una oferta
      operationId: createDeal
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDealInput'
      responses:
        '201':
          description: Oferta creada
          content:
            application/json:
              schema:
                type: object
                properties:
                  deal:
                    $ref: '#/components/schemas/Deal'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '409':
          description: Ya existe una oferta activa con esa URL (`duplicate_deal`, incluye `existingDealId`)
          content:
            application/json:
              schema:
                type: object
        '429':
          $ref: '#/components/responses/Error'

  /v1/deals/{id}:
    get:
      summary: Obtener una oferta
      operationId: getDeal
      parameters:
        - name: id
          in: path
          required: true
          schema: {type: string, format: uuid}
      responses:
        '200':
          description: Oferta
          content:
            application/json:
              schema:
                type: object
                properties:
                  deal:
                    $ref: '#/components/schemas/Deal'
        '404':
          $ref: '#/components/responses/Error'

  /v1/deals/{id}/comments:
    get:
      summary: Listar comentarios de una oferta
      description: Orden cronológico (más viejos primero), paginado por cursor.
      operationId: listComments
      parameters:
        - name: id
          in: path
          required: true
          schema: {type: string, format: uuid}
        - name: cursor
          in: query
          required: false
          schema: {type: string}
      responses:
        '200':
          description: Página de comentarios
          content:
            application/json:
              schema:
                type: object
                properties:
                  comments:
                    type: array
                    items:
                      $ref: '#/components/schemas/Comment'
                  nextCursor: {type: string}
    post:
      summary: Comentar una oferta
      operationId: createComment
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: {type: string, format: uuid}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body: {type: string, minLength: 1, maxLength: 1000}
      responses:
        '201':
          description: Comentario creado
          content:
            application/json:
              schema:
                type: object
                properties:
                  comment:
                    $ref: '#/components/schemas/Comment'
        '400':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'

  /v1/comments/{id}:
    delete:
      summary: Borrar un comentario propio
      operationId: deleteComment
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: {type: string, format: uuid}
      responses:
        '204':
          description: Comentario borrado
        '404':
          $ref: '#/components/responses/Error'

  /v1/categories:
    get:
      summary: Listar categorías
      operationId: listCategories
      responses:
        '200':
          description: Categorías disponibles
          content:
            application/json:
              schema:
                type: object
                properties:
                  categories:
                    type: array
                    items:
                      type: object
                      properties:
                        slug: {type: string, example: electrodomesticos}
                        name: {type: string, example: Electrodomésticos}

  /v1/feeds/principal:
    get:
      summary: Feed de ofertas recientes
      operationId: feedPrincipal
      parameters:
        - name: cursor
          in: query
          required: false
          schema: {type: string}
          description: Cursor opaco de la página anterior (`nextCursor`)
      responses:
        '200':
          description: Página de ofertas
          content:
            application/json:
              schema:
                type: object
                properties:
                  deals:
                    type: array
                    items:
                      $ref: '#/components/schemas/Deal'
                  nextCursor: {type: string}

  /v1/search:
    get:
      summary: Búsqueda de ofertas
      description: Full-text en español sobre título, descripción y tienda. Ordenado por recencia.
      operationId: searchDeals
      parameters:
        - name: q
          in: query
          required: true
          schema: {type: string}
          description: Términos de búsqueda (se combinan con AND)
        - name: cursor
          in: query
          required: false
          schema: {type: string}
          description: Cursor opaco de la página anterior (`nextCursor`)
      responses:
        '200':
          description: Página de ofertas que matchean
          content:
            application/json:
              schema:
                type: object
                properties:
                  deals:
                    type: array
                    items:
                      $ref: '#/components/schemas/Deal'
                  nextCursor: {type: string}
        '400':
          $ref: '#/components/responses/Error'

  /v1/unfurl:
    post:
      summary: Leer metadata Open Graph de un link
      description: >
        Fetch server-side de los og-tags (título, imagen, precio si hay) para
        autocompletar el form de publicación. Solo URLs http(s) públicas.
      operationId: unfurlUrl
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: {type: string, format: uri}
      responses:
        '200':
          description: Metadata extraída (campos best-effort, pueden venir vacíos)
          content:
            application/json:
              schema:
                type: object
                properties:
                  unfurl:
                    type: object
                    properties:
                      title: {type: string}
                      description: {type: string}
                      imageUrl: {type: string}
                      store: {type: string, description: 'og:site_name'}
                      price: {type: number, nullable: true}
                      currency: {type: string}
        '400':
          $ref: '#/components/responses/Error'
        '422':
          $ref: '#/components/responses/Error'

  /v1/uploads:
    post:
      summary: Pedir una signed URL para subir una imagen
      description: >
        Devuelve una URL firmada de GCS (15 min) para hacer PUT de la imagen
        directo al bucket, con content-type y tamaño (máx 5MB) firmados. El
        PUT debe incluir `requiredHeader` tal cual. Usar `publicUrl` como
        `imageUrl` al crear la oferta.
      operationId: createUpload
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contentType]
              properties:
                contentType: {type: string, enum: [image/jpeg, image/png, image/webp]}
      responses:
        '200':
          description: Signed URL emitida
          content:
            application/json:
              schema:
                type: object
                properties:
                  upload:
                    type: object
                    properties:
                      uploadUrl: {type: string}
                      publicUrl: {type: string}
                      expiresAt: {type: string, format: date-time}
                      maxSizeBytes: {type: integer}
                      requiredHeader: {type: string}
        '400':
          $ref: '#/components/responses/Error'
        '503':
          $ref: '#/components/responses/Error'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Access token obtenido en /v1/auth/token (scope deals:write)

  responses:
    Error:
      description: Error con envelope estándar
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code: {type: string, example: validation}
                  message: {type: string}

  schemas:
    CreateDealInput:
      type: object
      required: [url, title, description, categorySlug]
      properties:
        url: {type: string, format: uri}
        title: {type: string, minLength: 8, maxLength: 140}
        description: {type: string, description: 'Mínimo 5 palabras'}
        price: {type: number, nullable: true}
        originalPrice: {type: number, nullable: true}
        currency: {type: string, enum: [ARS, USD], default: ARS}
        categorySlug: {type: string}
        dealType: {type: string, enum: [product, promo], default: product}
        discountLabel: {type: string, nullable: true, example: '30% con Cuenta DNI'}
        store: {type: string, nullable: true}
        imageUrl: {type: string, nullable: true}
        expiresAt: {type: string, format: date-time, nullable: true}

    Comment:
      type: object
      properties:
        id: {type: string, format: uuid}
        dealId: {type: string, format: uuid}
        userId: {type: string, format: uuid}
        username: {type: string, nullable: true}
        body: {type: string}
        createdAt: {type: string, format: date-time}

    Deal:
      type: object
      properties:
        id: {type: string, format: uuid}
        title: {type: string}
        description: {type: string}
        url: {type: string}
        outPath: {type: string, example: /v1/out/8f14e45f-...}
        imageUrl: {type: string, nullable: true}
        price: {type: number, nullable: true}
        originalPrice: {type: number, nullable: true}
        currency: {type: string}
        dealType: {type: string, enum: [product, promo]}
        discountLabel: {type: string, nullable: true}
        store: {type: string, nullable: true}
        categorySlug: {type: string}
        status: {type: string, enum: [active, expired, removed, deleted]}
        source: {type: string, enum: [app, api]}
        posterId: {type: string}
        posterUsername: {type: string, nullable: true}
        upvotes: {type: integer}
        commentsCount: {type: integer}
        clicks: {type: integer}
        expiresAt: {type: string, format: date-time, nullable: true}
        createdAt: {type: string, format: date-time}
