openapi: 3.1.0

info:
  title: API Indik
  version: "1.0.0"
  description: |
    API REST para o ERP/PDV do comerciante operar o programa de indicações do Indik:
    validar, reservar e resgatar cupons, cadastrar clientes e ler os indicadores do painel.

    O manual de uso, com exemplos e o fluxo recomendado de caixa, está em `docs/API.md`.

    **Autenticação:** chave por loja no header `Authorization: Bearer {prefix}.{secret}`,
    gerada em `/painel/integracoes`.

    **Idempotência:** todo `POST` aceita o header `Idempotency-Key`; reenviar a mesma chave
    com o mesmo corpo devolve a resposta original (header `Idempotency-Replayed: true`) em vez
    de repetir a operação.
  contact:
    name: Suporte Indik
    url: https://indik.app.br/painel/suporte

servers:
  - url: https://indik.app.br/api/v1
    description: Produção
  - url: http://localhost:8080/api/v1
    description: Ambiente local

security:
  - bearerAuth: []

tags:
  - name: Diagnóstico
  - name: Cupons
  - name: Clientes
  - name: Indicações
  - name: Campanhas
  - name: Indicadores

paths:
  /ping:
    get:
      tags: [Diagnóstico]
      summary: Verifica a conectividade e a credencial
      description: Não exige escopo. Use como primeiro teste da integração.
      responses:
        "200":
          description: Credencial válida
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      tenant:
                        type: object
                        properties:
                          slug: { type: string, examples: ["minha-loja"] }
                          name: { type: string, examples: ["Minha Loja"] }
                          status: { type: string, enum: [pending, active, past_due, canceled] }
                      api_key:
                        type: object
                        properties:
                          name: { type: string, examples: ["PDV Loja Centro"] }
                          prefix: { type: string, examples: ["indk_live_8c17d1238ad36c3f"] }
                          scopes:
                            type: array
                            items: { type: string }
                      server_time: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "402": { $ref: "#/components/responses/SubscriptionInactive" }

  /coupons/{code}:
    get:
      tags: [Cupons]
      summary: Consulta um cupom pelo código
      description: |
        Sem efeito colateral — é o "validar" do caixa. O código não diferencia maiúsculas
        de minúsculas. Requer o escopo `coupons.read`.
      parameters:
        - $ref: "#/components/parameters/CouponCode"
      responses:
        "200": { $ref: "#/components/responses/CouponOk" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }
        "404":
          description: Cupom inexistente nesta loja (`coupon_not_found`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409": { $ref: "#/components/responses/CouponConflict" }

  /coupons/{code}/reserve:
    post:
      tags: [Cupons]
      summary: Reserva um cupom disponível
      description: |
        Trava o cupom durante a venda para que ele não seja usado em outro caixa.
        Só funciona a partir de `available`. Requer o escopo `coupons.write`.
      parameters:
        - $ref: "#/components/parameters/CouponCode"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200": { $ref: "#/components/responses/CouponOk" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }
        "404":
          description: Cupom inexistente nesta loja (`coupon_not_found`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409": { $ref: "#/components/responses/CouponConflict" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /coupons/{code}/redeem:
    post:
      tags: [Cupons]
      summary: Resgata um cupom
      description: |
        Aceita cupom `reserved` ou `available` (nesse caso a reserva é implícita).
        Requer o escopo `coupons.write`.

        Resgatar um cupom de boas-vindas (`origin: welcome`) confirma a indicação que o gerou e
        emite automaticamente o cupom de recompensa do indicador.
      parameters:
        - $ref: "#/components/parameters/CouponCode"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                sale_value:
                  type: number
                  minimum: 0
                  description: |
                    Valor total da venda em R$. Obrigatório na prática para cupons percentuais —
                    é dele que sai o `prize_value` e os indicadores financeiros.
                  examples: [150.0]
                store_id:
                  type: integer
                  description: Loja do resgate; gravado apenas se o cupom ainda não tiver loja.
                seller_id:
                  type: integer
                  description: Vendedor do resgate; mesma regra de `store_id`.
      responses:
        "200": { $ref: "#/components/responses/CouponOk" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }
        "404":
          description: Cupom inexistente nesta loja (`coupon_not_found`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409": { $ref: "#/components/responses/CouponConflict" }
        "422":
          description: |
            `below_min_purchase` (venda abaixo da compra mínima, com `details.min_purchase`)
            ou `validation_failed`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /coupons/{code}/release:
    post:
      tags: [Cupons]
      summary: Libera uma reserva
      description: |
        Devolve um cupom `reserved` para `available` — use quando a venda é cancelada, senão o
        cupom fica preso. Requer o escopo `coupons.write`.
      parameters:
        - $ref: "#/components/parameters/CouponCode"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200": { $ref: "#/components/responses/CouponOk" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }
        "404":
          description: Cupom inexistente nesta loja (`coupon_not_found`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: "`coupon_not_reserved` ou `coupon_already_redeemed`"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /customers:
    get:
      tags: [Clientes]
      summary: Lista clientes do programa
      description: Requer o escopo `customers.read`.
      parameters:
        - name: phone
          in: query
          schema: { type: string, maxLength: 20 }
          description: Aceita formatado; a comparação é por dígitos.
        - name: cpf
          in: query
          schema: { type: string, maxLength: 20 }
          description: Aceita formatado; a comparação é por dígitos.
        - name: email
          in: query
          schema: { type: string }
        - name: q
          in: query
          schema: { type: string, maxLength: 100 }
          description: Busca livre em nome, telefone e e-mail.
        - name: include_removed
          in: query
          schema: { type: boolean, default: false }
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: Lista paginada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Customer" }
                  meta: { $ref: "#/components/schemas/PaginationMeta" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }

    post:
      tags: [Clientes]
      summary: Cadastra um cliente no programa
      description: |
        Requer o escopo `customers.write`. Não passa por OTP — o ERP é um canal confiável —,
        por isso `accepted_terms` é obrigatório (LGPD).

        Informando um indicador (`referrer_invite_token`, `referrer_phone` ou `referrer_cpf`) e
        havendo campanha ativa, a indicação é aberta e o cupom de boas-vindas é emitido na hora.

        Retorna **201** quando o cliente é criado e **200** quando ele já participava do
        programa; `meta.created` diferencia os casos.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [accepted_terms]
              properties:
                name: { type: string, maxLength: 100 }
                phone:
                  type: string
                  pattern: "^[0-9]{10,11}$"
                  description: Obrigatório se `email` não for informado. Só dígitos, com DDD.
                email:
                  type: string
                  format: email
                  maxLength: 150
                  description: Obrigatório se `phone` não for informado.
                cpf: { type: string, minLength: 11, maxLength: 11 }
                accepted_terms:
                  type: boolean
                  const: true
                  description: Afirma que o aceite dos termos foi colhido junto ao cliente.
                store_id: { type: integer }
                seller_id: { type: integer }
                referrer_invite_token: { type: string, maxLength: 64 }
                referrer_phone: { type: string, maxLength: 20 }
                referrer_cpf: { type: string, maxLength: 11 }
      responses:
        "200":
          description: Cliente já participava do programa
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CustomerCreated" }
        "201":
          description: Cliente cadastrado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CustomerCreated" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /customers/{customer}:
    get:
      tags: [Clientes]
      summary: Detalha um cliente
      description: Inclui os cupons do cliente. Requer o escopo `customers.read`.
      parameters:
        - name: customer
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Cliente encontrado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Customer" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }
        "404": { $ref: "#/components/responses/NotFound" }

  /customers/{customer}/coupons:
    get:
      tags: [Clientes]
      summary: Lista os cupons de um cliente
      description: Requer o escopo `coupons.read`.
      parameters:
        - name: customer
          in: path
          required: true
          schema: { type: integer }
        - name: status
          in: query
          schema:
            type: string
            enum: [available, reserved, redeemed, expired]
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: Lista paginada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Coupon" }
                  meta: { $ref: "#/components/schemas/PaginationMeta" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }
        "404": { $ref: "#/components/responses/NotFound" }

  /referrals:
    get:
      tags: [Indicações]
      summary: Lista indicações
      description: Requer o escopo `referrals.read`.
      parameters:
        - name: status
          in: query
          schema: { type: string, enum: [pending, reserved, used, expired] }
        - name: campaign_id
          in: query
          schema: { type: integer }
        - name: referrer_id
          in: query
          schema: { type: integer }
        - name: from
          in: query
          schema: { type: string, format: date }
        - name: to
          in: query
          schema: { type: string, format: date }
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: Lista paginada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Referral" }
                  meta: { $ref: "#/components/schemas/PaginationMeta" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }

  /campaigns:
    get:
      tags: [Campanhas]
      summary: Lista campanhas
      description: |
        Requer o escopo `campaigns.read`. Use para ler a compra mínima vigente e validar a venda
        no próprio ERP antes de chamar o resgate.
      parameters:
        - name: active_only
          in: query
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Campanhas da loja
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Campaign" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }

  /stores:
    get:
      tags: [Campanhas]
      summary: Lista lojas
      description: |
        Requer o escopo `campaigns.read`. É a fonte dos `store_id` usados no resgate e no
        cadastro de clientes.
      responses:
        "200":
          description: Lojas da conta
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Store" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }

  /metrics:
    get:
      tags: [Indicadores]
      summary: Indicadores do programa
      description: |
        Os mesmos números de `/painel/dashboard`, em formato cru. Requer o escopo `metrics.read`.

        Cada métrica traz o valor do período, o do período imediatamente anterior e a variação
        percentual. As séries em `series` têm um ponto por dia, em ordem cronológica.
      parameters:
        - name: period
          in: query
          schema: { type: string, enum: ["7d", "30d", "90d"], default: "30d" }
        - name: from
          in: query
          schema: { type: string, format: date }
          description: Intervalo explícito; quando presente, ignora `period`. Máximo 366 dias.
        - name: to
          in: query
          schema: { type: string, format: date }
        - name: campaign_id
          in: query
          schema: { type: integer }
      responses:
        "200":
          description: Indicadores do período
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Metrics" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/InsufficientScope" }
        "422": { $ref: "#/components/responses/ValidationFailed" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Chave de API da loja, no formato `{prefix}.{secret}`, gerada em `/painel/integracoes`.
        O segredo é exibido uma única vez na criação.

        A chave pode ter validade (para de funcionar na data escolhida) e lista de endereços
        autorizados (IP exato ou faixa CIDR) — ambas opcionais, definidas na criação. Chamada
        de origem não autorizada recebe `403 ip_not_allowed`.

        Tentativas de autenticação recusadas são limitadas a 10 a cada 5 minutos por IP.

  parameters:
    CouponCode:
      name: code
      in: path
      required: true
      schema: { type: string, maxLength: 64 }
      description: Código do cupom; não diferencia maiúsculas de minúsculas.
      examples:
        exemplo:
          value: ABC12345

    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 128 }
      description: |
        Identificador único da operação no ERP (nº do cupom fiscal, ID do pedido). Reenviar a
        mesma chave com o mesmo corpo devolve a resposta original em vez de repetir a operação.
        Janela de replay: 24 horas.

    Page:
      name: page
      in: query
      schema: { type: integer, minimum: 1, default: 1 }

    PerPage:
      name: per_page
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }

  responses:
    CouponOk:
      description: Cupom
      content:
        application/json:
          schema:
            type: object
            properties:
              data: { $ref: "#/components/schemas/Coupon" }

    CouponConflict:
      description: |
        Estado do cupom incompatível com a operação: `coupon_already_redeemed`,
        `coupon_expired` ou `coupon_unavailable`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

    Unauthenticated:
      description: "`unauthenticated` (header ausente) ou `invalid_api_key` (chave inválida, revogada ou expirada)"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

    SubscriptionInactive:
      description: "`subscription_inactive` — assinatura da loja fora de dia"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

    InsufficientScope:
      description: |
        `insufficient_scope` (a chave não tem o escopo exigido) ou `ip_not_allowed`
        (a chave restringe endereços e a chamada veio de outro).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

    NotFound:
      description: "`not_found` — recurso inexistente ou de outra loja"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

    ValidationFailed:
      description: "`validation_failed` — `details` traz os campos e as mensagens"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

    IdempotencyConflict:
      description: "`idempotency_conflict` ou `idempotency_in_progress`"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

    RateLimited:
      description: "`rate_limited` — veja os headers `Retry-After` e `X-RateLimit-*`"
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Contrato estável — programe suas condições contra este campo.
              enum:
                - unauthenticated
                - invalid_api_key
                - subscription_inactive
                - insufficient_scope
                - ip_not_allowed
                - not_found
                - coupon_not_found
                - coupon_already_redeemed
                - coupon_expired
                - coupon_unavailable
                - coupon_not_reserved
                - below_min_purchase
                - validation_failed
                - idempotency_conflict
                - idempotency_in_progress
                - rate_limited
                - method_not_allowed
                - forbidden
                - http_error
                - server_error
            message:
              type: string
              description: Texto em pt-BR pronto para exibir ao operador; pode mudar sem aviso.
            details:
              type: object
              description: Contexto adicional. Em `validation_failed`, os campos e suas mensagens.
      examples:
        - error:
            code: coupon_already_redeemed
            message: Este cupom já foi utilizado.
            details: {}

    PaginationMeta:
      type: object
      properties:
        page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }
        last_page: { type: integer }

    Coupon:
      type: object
      properties:
        code: { type: string, examples: ["ABC12345"] }
        status:
          type: string
          enum: [available, reserved, redeemed, expired]
        type:
          type: string
          enum: [percent, fixed, cashback, product]
        origin:
          type: string
          enum: [welcome, referral, milestone, manual]
          description: |
            `welcome` = boas-vindas (resgatá-lo confirma a indicação);
            `referral` = recompensa do indicador; `milestone` = meta batida; `manual` = emitido no painel.
        value:
          type: number
          description: Percentual quando `type = percent`; valor em R$ nos demais casos.
        value_label:
          type: string
          description: Rótulo pronto para exibir ao operador.
          examples: ["10% OFF"]
        product:
          type: [object, "null"]
          properties:
            name: { type: [string, "null"] }
            description: { type: [string, "null"] }
            image_url: { type: [string, "null"] }
        sale_value:
          type: [number, "null"]
          description: Valor da venda informado no resgate.
        prize_value:
          type: [number, "null"]
          description: Desconto efetivo em R$ apurado no resgate.
        campaign_id: { type: [integer, "null"] }
        customer_id: { type: [integer, "null"] }
        referral_id: { type: [integer, "null"] }
        store_id: { type: [integer, "null"] }
        seller_id: { type: [integer, "null"] }
        min_purchase:
          type: [number, "null"]
          description: Compra mínima da campanha, quando a campanha está carregada.
        customer:
          type: [object, "null"]
          properties:
            id: { type: integer }
            name: { type: [string, "null"] }
            phone: { type: [string, "null"] }
            email: { type: [string, "null"] }
        expires_at: { type: [string, "null"], format: date-time }
        reserved_at: { type: [string, "null"], format: date-time }
        redeemed_at: { type: [string, "null"], format: date-time }
        created_at: { type: [string, "null"], format: date-time }

    Customer:
      type: object
      properties:
        id: { type: integer }
        name: { type: [string, "null"] }
        phone: { type: [string, "null"] }
        email: { type: [string, "null"] }
        cpf: { type: [string, "null"] }
        invite_token: { type: string }
        invite_url:
          type: [string, "null"]
          description: Link pronto para o cliente indicar amigos.
        referrer_id: { type: [integer, "null"] }
        store_id: { type: [integer, "null"] }
        seller_id: { type: [integer, "null"] }
        accepted_terms_at: { type: [string, "null"], format: date-time }
        removed_at: { type: [string, "null"], format: date-time }
        created_at: { type: [string, "null"], format: date-time }
        coupons:
          type: array
          items: { $ref: "#/components/schemas/Coupon" }

    CustomerCreated:
      type: object
      properties:
        data: { $ref: "#/components/schemas/Customer" }
        meta:
          type: object
          properties:
            created:
              type: boolean
              description: "`false` quando o cliente já participava do programa."
            referral_id:
              type: [integer, "null"]
              description: Indicação aberta neste cadastro, se houve.

    Referral:
      type: object
      properties:
        id: { type: integer }
        status:
          type: string
          enum: [pending, reserved, used, expired]
        campaign_id: { type: [integer, "null"] }
        store_id: { type: [integer, "null"] }
        seller_id: { type: [integer, "null"] }
        referrer:
          type: [object, "null"]
          properties:
            id: { type: integer }
            name: { type: [string, "null"] }
            phone: { type: [string, "null"] }
        referred:
          type: [object, "null"]
          description: Cliente indicado, quando já cadastrado.
          properties:
            id: { type: integer }
            name: { type: [string, "null"] }
            phone: { type: [string, "null"] }
        referred_contact:
          type: object
          description: Dados soltos de um convite direto que ainda não virou cadastro.
          properties:
            name: { type: [string, "null"] }
            phone: { type: [string, "null"] }
            email: { type: [string, "null"] }
        coupon: { $ref: "#/components/schemas/Coupon" }
        created_at: { type: [string, "null"], format: date-time }
        updated_at: { type: [string, "null"], format: date-time }

    Campaign:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        status:
          type: string
          enum: [draft, active, paused, ended]
        starts_at: { type: [string, "null"], format: date }
        ends_at: { type: [string, "null"], format: date }
        min_purchase:
          type: [number, "null"]
          description: Compra mínima para resgate — valide no ERP antes de fechar a venda.
        coupon_validity_days: { type: [integer, "null"] }
        referrer_reward_enabled: { type: boolean }
        referrer_reward_label: { type: [string, "null"] }
        welcome_enabled: { type: boolean }
        welcome_reward_label: { type: [string, "null"] }
        max_coupons_total: { type: [integer, "null"] }
        max_coupons_per_customer: { type: [integer, "null"] }
        milestones:
          type: array
          items:
            type: object
            properties:
              referrals_needed: { type: integer }
              reward_description: { type: string }
              reward_value: { type: [number, "null"] }
        created_at: { type: [string, "null"], format: date-time }

    Store:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        address: { type: [string, "null"] }
        phone: { type: [string, "null"] }
        is_active: { type: boolean }

    Metric:
      type: object
      properties:
        current: { type: number }
        previous:
          type: number
          description: Mesmo indicador no período imediatamente anterior.
        delta:
          type: number
          description: Variação percentual absoluta em relação ao período anterior.
        up:
          type: boolean
          description: "`true` quando a variação foi de alta."

    Metrics:
      type: object
      properties:
        period:
          type: object
          properties:
            start: { type: string, format: date-time }
            end: { type: string, format: date-time }
            previous_start: { type: string, format: date-time }
            previous_end: { type: string, format: date-time }
            days: { type: integer }
        metrics:
          type: object
          properties:
            referrals: { $ref: "#/components/schemas/Metric" }
            converted_referrals: { $ref: "#/components/schemas/Metric" }
            conversion_rate: { $ref: "#/components/schemas/Metric" }
            redeemed_coupons: { $ref: "#/components/schemas/Metric" }
            redeemed_value: { $ref: "#/components/schemas/Metric" }
            participants: { $ref: "#/components/schemas/Metric" }
            unique_referrers: { $ref: "#/components/schemas/Metric" }
            penetration_rate: { $ref: "#/components/schemas/Metric" }
            viral_effect: { $ref: "#/components/schemas/Metric" }
            influence_rate: { $ref: "#/components/schemas/Metric" }
            redemption_rate: { $ref: "#/components/schemas/Metric" }
            sales_value: { $ref: "#/components/schemas/Metric" }
            campaign_cost: { $ref: "#/components/schemas/Metric" }
            avg_ticket: { $ref: "#/components/schemas/Metric" }
            cac_per_referral: { $ref: "#/components/schemas/Metric" }
        series:
          type: object
          description: Um ponto por dia do período, em ordem cronológica.
          properties:
            referrals_daily:
              type: array
              items: { type: integer }
            converted_daily:
              type: array
              items: { type: integer }
            pending_daily:
              type: array
              items: { type: integer }
            redeemed_daily:
              type: array
              items: { type: integer }
        active_campaign:
          type: [object, "null"]
          properties:
            id: { type: integer }
            name: { type: string }
            days_left: { type: [integer, "null"] }
            reward_referrer: { type: string }
            reward_referred: { type: string }
            goal: { type: [integer, "null"] }
            progress: { type: integer }
            progress_pct: { type: [integer, "null"] }
            active_referrers: { type: integer }
            total_rewards: { type: number }
        usage:
          type: array
          description: Consumo do mês corrente contra os limites do plano.
          items:
            type: object
            properties:
              key: { type: string }
              label: { type: string }
              used: { type: integer }
              limit: { type: integer }
              unlimited: { type: boolean }
              pct: { type: [integer, "null"] }
              state: { type: string, enum: [ok, warning, danger] }
