openapi: 3.1.0
info:
  title: StudioW Public API
  description: |
    API de leitura do StudioW Data Warehouse — integração entre Trinx (gestão de salão) e Kamino (ERP financeiro).

    ## O que há de novo

    ### 20/07/2026
    - Novo endpoint `GET /v1/pagamentos-kamino` e `GET /v1/pagamentos-kamino/{id}`: todos os pagamentos do Kamino (pagos e pendentes, qualquer categoria/centro de custo — inclui despesas operacionais como aluguel, água, luz, condomínio), independente da integração Trinx↔Kamino. Cada item traz `situacao` (`1`=pendente, `2`=pago) e um campo `pago` (boolean) já calculado. Suporta os mesmos dois modos de paginação de `/v1/lancamentos`.

    ### 07/07/2026
    - `GET /v1/lancamentos` agora retorna `raw_data` completo na listagem (antes só vinha em `GET /v1/lancamentos/{id}`)
    - Nova paginação por cursor (`after_id`) em `GET /v1/lancamentos` — sem `COUNT(*)`/`OFFSET`, ideal para sincronizar/importar a tabela inteira. Veja o parâmetro `after_id` no endpoint.

    Histórico completo: [CHANGELOG.md](https://github.com/Academia-Code/api-dwStw/blob/main/CHANGELOG.md)

    ## Autenticação

    A API usa JWT com dois tokens:
    - **access_token**: token de curta duração (15 min) enviado no header `Authorization: Bearer <token>`
    - **refresh_token**: token de longa duração (7 dias) usado para renovar o access_token sem novo login

    ### Fluxo padrão
    1. `POST /auth/login` → receba `access_token` + `refresh_token`
    2. Use `Authorization: Bearer <access_token>` em todas as requisições
    3. Quando o access_token expirar (HTTP 401), chame `POST /auth/refresh` com o `refresh_token`
    4. Ao terminar a sessão, chame `POST /auth/logout`

    ## Paginação

    Endpoints de lista retornam:
    ```json
    {
      "data": [...],
      "pagination": {
        "page": 1,
        "per_page": 50,
        "total": 320,
        "total_pages": 7
      }
    }
    ```
    Use os query params `page` e `per_page` para navegar.

    ## Erros

    Todos os erros seguem o formato:
    ```json
    {
      "error": "Unauthorized",
      "message": "Descrição legível do erro"
    }
    ```

  version: 1.0.0
  contact:
    name: StudioW
    email: richardmsenra@gmail.com

servers:
  - url: http://localhost:4000
    description: Desenvolvimento local
  - url: https://api.studiow.com.br
    description: Produção

security:
  - BearerAuth: []

tags:
  - name: auth
    description: Autenticação JWT
  - name: transacoes
    description: Transações capturadas do Trinx
  - name: lancamentos
    description: Lançamentos enviados ao Trinx via Kamino
  - name: pagamentos-kamino
    description: Todos os pagamentos do Kamino (pagos e pendentes, qualquer categoria/centro de custo), independente da integração Trinx
  - name: profissionais
    description: Mapeamento de profissionais Trinx ↔ Kamino
  - name: estabelecimentos
    description: Mapeamento de estabelecimentos Trinx ↔ Kamino
  - name: jobs-log
    description: Histórico de execução de jobs
  - name: relatorios
    description: Relatórios e resumos agregados

paths:

  # ─── Health ─────────────────────────────────────────────────────────────────

  /health:
    get:
      summary: Health check
      description: Verifica se a API está operacional
      security: []
      tags: [auth]
      responses:
        "200":
          description: API operacional
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  version:
                    type: string
                    example: "1.0.0"
                  uptime:
                    type: number
                    example: 3600.5
                  timestamp:
                    type: string
                    format: date-time

  # ─── Auth ────────────────────────────────────────────────────────────────────

  /auth/login:
    post:
      summary: Login
      description: Autentica com email e senha. Retorna access_token (15 min) e refresh_token (7 dias).
      security: []
      tags: [auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                  example: usuario@studiow.com.br
                password:
                  type: string
                  example: "senha_segura_123"
      responses:
        "200":
          description: Login realizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /auth/refresh:
    post:
      summary: Renovar access token
      description: |
        Troca o refresh_token por um novo par de tokens. O refresh_token antigo é revogado imediatamente
        (rotação de tokens — cada refresh_token só pode ser usado uma vez).
      security: []
      tags: [auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refresh_token]
              properties:
                refresh_token:
                  type: string
                  example: "abc123def456..."
      responses:
        "200":
          description: Tokens renovados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /auth/logout:
    post:
      summary: Logout
      description: Revoga o refresh_token. O access_token expira naturalmente em até 15 min.
      tags: [auth]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                refresh_token:
                  type: string
      responses:
        "200":
          description: Logout realizado
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Logout realizado com sucesso

  /auth/me:
    get:
      summary: Usuário autenticado
      description: Retorna dados do usuário associado ao access_token atual.
      tags: [auth]
      responses:
        "200":
          description: Dados do usuário
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ─── Transações ──────────────────────────────────────────────────────────────

  /v1/transacoes:
    get:
      summary: Listar transações
      description: |
        Retorna transações capturadas do Trinx (view `trusted.transacoes`).
        Cada transação representa um atendimento no salão com dados de pagamento.
      tags: [transacoes]
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: data_inicio
          in: query
          schema:
            type: string
            format: date-time
            example: "2024-01-01T00:00:00-03:00"
        - name: data_fim
          in: query
          schema:
            type: string
            format: date-time
            example: "2024-01-31T23:59:59-03:00"
        - name: estabelecimento_id
          in: query
          schema:
            type: string
            example: "12345"
        - name: sent_to_kamino
          in: query
          description: Filtrar por status de envio ao Kamino
          schema:
            type: string
            enum: [true, false]
      responses:
        "200":
          description: Lista de transações
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Transacao"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/transacoes/{id}:
    get:
      summary: Buscar transação por ID
      description: Retorna uma transação pelo `trinx_id`, incluindo o `raw_data` completo da API Trinx.
      tags: [transacoes]
      parameters:
        - name: id
          in: path
          required: true
          description: trinx_id da transação
          schema:
            type: string
            example: "TRX-123456"
      responses:
        "200":
          description: Transação encontrada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TransacaoDetalhada"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── Lançamentos ─────────────────────────────────────────────────────────────

  /v1/lancamentos:
    get:
      summary: Listar lançamentos
      description: |
        Retorna lançamentos enviados ao Trinx originados de pagamentos no Kamino,
        incluindo o `raw_data` completo de cada lançamento (mesmo conteúdo que
        `GET /v1/lancamentos/{id}` retorna). Cada lançamento representa uma
        comissão ou débito registrado no sistema do salão.

        Suporta dois modos de paginação:
        - **Clássico** (`page`/`per_page`): retorna `total`/`total_pages`.
          Bom para telas com navegação de página.
        - **Cursor** (`after_id`): sem `COUNT(*)`/`OFFSET`, ideal para
          sync/importação completa da tabela. Veja o parâmetro `after_id`.
      tags: [lancamentos]
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/after_id"
        - name: data_inicio
          in: query
          schema:
            type: string
            format: date-time
        - name: data_fim
          in: query
          schema:
            type: string
            format: date-time
        - name: estabelecimento_id
          in: query
          schema:
            type: string
        - name: sent_to_trinx
          in: query
          schema:
            type: string
            enum: [true, false]
      responses:
        "200":
          description: |
            Lista de lançamentos. O formato de `pagination` depende do modo:
            `page`/`per_page` (padrão) ou `after_id` (cursor) — ver descrição acima.
          content:
            application/json:
              schema:
                oneOf:
                  - allOf:
                      - $ref: "#/components/schemas/PaginatedResponse"
                      - type: object
                        properties:
                          data:
                            type: array
                            items:
                              $ref: "#/components/schemas/Lancamento"
                  - allOf:
                      - $ref: "#/components/schemas/CursorPaginatedResponse"
                      - type: object
                        properties:
                          data:
                            type: array
                            items:
                              $ref: "#/components/schemas/Lancamento"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/lancamentos/{id}:
    get:
      summary: Buscar lançamento por ID
      tags: [lancamentos]
      parameters:
        - name: id
          in: path
          required: true
          description: trinx_id do lançamento
          schema:
            type: string
      responses:
        "200":
          description: Lançamento encontrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LancamentoDetalhado"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── Pagamentos Kamino ───────────────────────────────────────────────────────

  /v1/pagamentos-kamino:
    get:
      summary: Listar pagamentos do Kamino
      description: |
        Retorna TODOS os pagamentos do Kamino — pagos e pendentes, qualquer
        categoria/centro de custo, incluindo despesas operacionais do salão
        (aluguel, água, luz, condomínio, manutenção etc.) que não passam pela
        integração Trinx↔Kamino. Independente dela: nada aqui é enviado a
        lugar nenhum, é só leitura. Cada item traz `situacao` (bruto) e um
        campo `pago` (boolean) já calculado a partir dele.

        Suporta os mesmos dois modos de paginação de `/v1/lancamentos`:
        - **Clássico** (`page`/`per_page`): retorna `total`/`total_pages`.
        - **Cursor** (`after_id`): sem `COUNT(*)`/`OFFSET`, ideal para
          sync/importação completa da tabela. Veja o parâmetro `after_id`.
      tags: [pagamentos-kamino]
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/after_id"
        - name: situacao
          in: query
          description: "1 = pendente, 2 = pago"
          schema:
            type: string
            enum: ["1", "2"]
        - name: id_centro_custo
          in: query
          schema:
            type: string
        - name: nome_conta_classificacao
          in: query
          schema:
            type: string
        - name: data_inicio
          in: query
          schema:
            type: string
            format: date-time
        - name: data_fim
          in: query
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: |
            Lista de pagamentos do Kamino. O formato de `pagination` depende
            do modo: `page`/`per_page` (padrão) ou `after_id` (cursor).
          content:
            application/json:
              schema:
                oneOf:
                  - allOf:
                      - $ref: "#/components/schemas/PaginatedResponse"
                      - type: object
                        properties:
                          data:
                            type: array
                            items:
                              $ref: "#/components/schemas/PagamentoKamino"
                  - allOf:
                      - $ref: "#/components/schemas/CursorPaginatedResponse"
                      - type: object
                        properties:
                          data:
                            type: array
                            items:
                              $ref: "#/components/schemas/PagamentoKamino"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/pagamentos-kamino/{id}:
    get:
      summary: Buscar pagamento do Kamino por ID
      tags: [pagamentos-kamino]
      parameters:
        - name: id
          in: path
          required: true
          description: kamino_id do pagamento
          schema:
            type: integer
      responses:
        "200":
          description: Pagamento encontrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PagamentoKaminoDetalhado"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── Profissionais ───────────────────────────────────────────────────────────

  /v1/profissionais:
    get:
      summary: Listar profissionais
      description: |
        Retorna o mapeamento de profissionais entre Trinx e Kamino.
        Cada entrada relaciona um profissional do salão com seu centro de custo no ERP.
      tags: [profissionais]
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: ativo
          in: query
          schema:
            type: string
            enum: [true, false]
        - name: estabelecimento_id
          in: query
          description: trinx_estabelecimento_id
          schema:
            type: string
      responses:
        "200":
          description: Lista de profissionais
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Profissional"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/produtos:
    get:
      summary: Listar catálogo de produtos
      description: |
        Catálogo de produtos cadastrados por unidade.

        Sincronizado **uma vez por semana**, domingo às 05h de Brasília, porque catálogo
        muda pouco e cada varredura completa consome cota da API da Trinks. Portanto é
        normal o campo `synced_at` estar alguns dias atrás: isso não indica falha.

        Cobre as **onze** unidades: os seis salões e as cinco lojas. As lojas entraram no
        catálogo em 27/08/2026. Elas aparecem aqui, mas não participam da integração
        financeira, então não têm transação nem lançamento associados.
      tags: [produtos]
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: estabelecimento_id
          in: query
          description: trinx_estabelecimento_id da unidade
          schema:
            type: string
        - name: nome
          in: query
          description: busca parcial no nome do produto, sem diferenciar maiúsculas
          schema:
            type: string
      responses:
        "200":
          description: Lista de produtos
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Produto"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/vendas-produto:
    get:
      summary: Listar vendas de produto
      description: |
        Produtos que saíram em transações, um item por linha. Vem junto das transações,
        sincronizadas de hora em hora, e **não** do catálogo semanal.

        Duas coisas a saber antes de interpretar o resultado:

        1. O volume é baixo e intermitente. A maioria das transações não tem produto, e
           dias inteiros sem nenhuma venda são normais.
        2. Boa parte do que aparece é produto **descontado do profissional**, e não venda
           ao cliente. Use o campo `forma_pagamento` para separar: o valor
           `"Descontar do Profissional"` identifica esse caso.

        3. Salão e loja vêm juntos. O campo `origem` diz qual é qual, e o parâmetro de
           mesmo nome filtra. Sem filtro, a resposta traz os dois, porque a pergunta
           "quanto vendemos de produto" normalmente não distingue.

        As duas origens vivem em tabelas separadas no banco, por uma razão de segurança do
        fluxo financeiro e não de modelagem. Para quem lê, respondem a mesma pergunta, então
        a API as apresenta unificadas. O perfil delas é bem diferente: no salão a venda de
        produto é exceção, na loja é quase toda transação.
      tags: [produtos]
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: estabelecimento_id
          in: query
          schema:
            type: string
        - name: data_inicio
          in: query
          description: ISO 8601 com fuso, filtra por data/hora da transação
          schema:
            type: string
            format: date-time
        - name: data_fim
          in: query
          schema:
            type: string
            format: date-time
        - name: origem
          in: query
          description: Restringe a salão ou a loja. Omitido, traz os dois.
          schema:
            type: string
            enum: [salao, loja]
      responses:
        "200":
          description: Lista de vendas de produto
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/VendaProduto"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/profissionais/{id}:
    get:
      summary: Buscar profissional por ID
      tags: [profissionais]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            example: 1
      responses:
        "200":
          description: Profissional encontrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Profissional"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── Estabelecimentos ────────────────────────────────────────────────────────

  /v1/estabelecimentos:
    get:
      summary: Listar estabelecimentos
      description: Retorna o mapeamento de estabelecimentos (unidades do salão) entre Trinx e Kamino.
      tags: [estabelecimentos]
      parameters:
        - name: ativo
          in: query
          schema:
            type: string
            enum: [true, false]
      responses:
        "200":
          description: Lista de estabelecimentos
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Estabelecimento"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/estabelecimentos/{id}:
    get:
      summary: Buscar estabelecimento por ID
      tags: [estabelecimentos]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Estabelecimento encontrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Estabelecimento"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── Jobs Log ────────────────────────────────────────────────────────────────

  /v1/jobs-log:
    get:
      summary: Histórico de jobs
      description: |
        Retorna o histórico de execuções dos workers de sincronização.
        Jobs disponíveis: `sync-transacoes`, `send-receber`, `send-pagar`.
      tags: [jobs-log]
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: job_name
          in: query
          schema:
            type: string
            enum: [sync-transacoes, send-receber, send-pagar]
        - name: status
          in: query
          schema:
            type: string
            enum: [started, completed, failed]
        - name: data_inicio
          in: query
          schema:
            type: string
            format: date-time
        - name: data_fim
          in: query
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Histórico de jobs
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/JobLog"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ─── Relatórios ──────────────────────────────────────────────────────────────

  /v1/relatorios/resumo:
    get:
      summary: Resumo geral
      description: |
        Retorna métricas consolidadas: total de transações, valor total, status de envio ao Kamino
        e performance dos jobs nas últimas 24 horas.
      tags: [relatorios]
      parameters:
        - name: data_inicio
          in: query
          schema:
            type: string
            format: date-time
        - name: data_fim
          in: query
          schema:
            type: string
            format: date-time
        - name: estabelecimento_id
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Resumo calculado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Resumo"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/relatorios/transacoes-por-profissional:
    get:
      summary: Transações agrupadas por profissional
      description: Retorna volume e valor total de transações por profissional no período.
      tags: [relatorios]
      parameters:
        - name: data_inicio
          in: query
          schema:
            type: string
            format: date-time
        - name: data_fim
          in: query
          schema:
            type: string
            format: date-time
        - name: estabelecimento_id
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Dados agrupados
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/TransacaoPorProfissional"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/relatorios/transacoes-por-forma-pagamento:
    get:
      summary: Transações agrupadas por forma de pagamento
      description: |
        Retorna volume, valor total, ticket médio e taxa aplicada por forma de pagamento.
        Inclui cruzamento com a tabela de taxas de cartão quando disponível.
      tags: [relatorios]
      parameters:
        - name: data_inicio
          in: query
          schema:
            type: string
            format: date-time
        - name: data_fim
          in: query
          schema:
            type: string
            format: date-time
        - name: estabelecimento_id
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Dados agrupados
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/TransacaoPorFormaPagamento"
        "401":
          $ref: "#/components/responses/Unauthorized"

# ─── Components ───────────────────────────────────────────────────────────────

components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "Access token obtido via POST /auth/login. Expira em 15 minutos."

  parameters:
    page:
      name: page
      in: query
      description: Número da página (começa em 1)
      schema:
        type: integer
        minimum: 1
        default: 1
    per_page:
      name: per_page
      in: query
      description: Itens por página (máx 200)
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    after_id:
      name: after_id
      in: query
      description: |
        Ativa a paginação por cursor (em vez de `page`/offset). Retorna apenas
        registros com `id` maior que o valor informado, ordenados por `id`
        crescente. Use `after_id=0` na primeira chamada e, nas seguintes,
        o valor de `pagination.next_after_id` retornado.

        Recomendado para varrer a tabela inteira (sync/importação completa):
        não executa `COUNT(*)` nem `OFFSET`, então o tempo de resposta não
        piora conforme a tabela cresce ou a página avança. Quando presente,
        este parâmetro tem prioridade sobre `page` e a resposta não inclui
        `total`/`total_pages`.
      schema:
        type: integer
        minimum: 0

  responses:
    Unauthorized:
      description: Token ausente, inválido ou expirado
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Unauthorized
            message: Token inválido ou expirado. Renove usando POST /auth/refresh
    NotFound:
      description: Recurso não encontrado
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    BadRequest:
      description: Parâmetros inválidos
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:

    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          example: Unauthorized
        message:
          type: string
          example: Token inválido ou expirado

    TokenResponse:
      type: object
      properties:
        access_token:
          type: string
          description: JWT de acesso. Válido por 15 minutos.
          example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
        refresh_token:
          type: string
          description: Token opaco de renovação. Válido por 7 dias. Uso único.
          example: "a3f2b1c4d5e6f7a8b9c0..."
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          description: Validade do access_token em segundos
          example: 900

    User:
      type: object
      properties:
        id:
          type: integer
          example: 1
        email:
          type: string
          format: email
          example: usuario@studiow.com.br
        name:
          type: string
          example: Richard Senra

    PaginatedResponse:
      type: object
      properties:
        pagination:
          type: object
          properties:
            page:
              type: integer
              example: 1
            per_page:
              type: integer
              example: 50
            total:
              type: integer
              example: 320
            total_pages:
              type: integer
              example: 7

    CursorPaginatedResponse:
      description: Formato de paginação retornado quando o parâmetro `after_id` é usado.
      type: object
      properties:
        pagination:
          type: object
          properties:
            per_page:
              type: integer
              example: 50
            next_after_id:
              type: integer
              nullable: true
              description: Use como `after_id` na próxima chamada. `null` quando não há mais registros.
              example: 132
            has_more:
              type: boolean
              example: true

    Transacao:
      type: object
      properties:
        trinx_id:
          type: string
          example: "TRX-123456"
        estabelecimento_id:
          type: string
          example: "12345"
        total:
          type: string
          description: Valor total em reais (string numérica)
          example: "150.00"
        data_hora:
          type: string
          format: date-time
          example: "2024-03-15T14:30:00-03:00"
        cliente_trinx_id:
          type: string
          nullable: true
          example: "98765"
        cliente_nome:
          type: string
          nullable: true
          example: "Maria Silva"
        forma_pagamento_principal:
          type: string
          nullable: true
          example: "Cartão de Crédito"
        valor_pagamento:
          type: string
          nullable: true
          example: "150.00"
        parcelas:
          type: integer
          nullable: true
          example: 1
        qtd_servicos:
          type: integer
          nullable: true
          example: 2
        servicos:
          type: array
          nullable: true
          description: >-
            Lista de serviços da transação, conforme retornado pela Trinx,
            enriquecida com profissional_nome e kamino_centro_custo_id
            (cruzados via idProfissionalQueRealizouServico contra
            mapeamento_profissionais; vêm null se o profissional não estiver mapeado)
          items:
            type: object
            properties:
              idProfissionalQueRealizouServico:
                type: integer
                nullable: true
              profissional_nome:
                type: string
                nullable: true
                example: "Fulana da Silva"
              kamino_centro_custo_id:
                type: string
                nullable: true
                example: "37"
            additionalProperties: true
        sent_to_kamino:
          type: boolean
          example: true
        synced_at:
          type: string
          format: date-time

    TransacaoDetalhada:
      allOf:
        - type: object
          properties:
            id:
              type: integer
            kamino_recebimento_id:
              type: string
              nullable: true
            sent_at:
              type: string
              format: date-time
              nullable: true
            raw_data:
              type: object
              description: Objeto JSON completo retornado pela API Trinx

    Lancamento:
      type: object
      properties:
        id:
          type: integer
          example: 1
        trinx_id:
          type: string
          example: "LAN-654321"
        estabelecimento_id:
          type: string
          example: "12345"
        raw_data:
          type: object
          description: Objeto JSON completo do lançamento, como retornado pelo Kamino/Trinx.
        synced_at:
          type: string
          format: date-time
        sent_to_trinx:
          type: boolean
          example: true
        sent_at:
          type: string
          format: date-time
          nullable: true

    LancamentoDetalhado:
      allOf:
        - $ref: "#/components/schemas/Lancamento"

    PagamentoKamino:
      type: object
      properties:
        id:
          type: integer
          example: 1
        kamino_id:
          type: integer
          example: 332
        id_centro_custo:
          type: string
          nullable: true
          example: "10"
        situacao:
          type: integer
          description: "1 = pendente, 2 = pago"
          example: 2
        pago:
          type: boolean
          description: Calculado a partir de situacao (situacao === 2)
          example: true
        nome_conta_classificacao:
          type: string
          nullable: true
          example: "Aluguel"
        id_plano_conta_classificacao:
          type: integer
          nullable: true
        valor:
          type: string
          nullable: true
          example: "3500.00"
        data_vencimento:
          type: string
          format: date
          nullable: true
        data_pagamento:
          type: string
          format: date
          nullable: true
        descricao:
          type: string
          nullable: true
        raw_data:
          type: object
          description: Objeto JSON completo retornado pela API Kamino
        synced_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    PagamentoKaminoDetalhado:
      allOf:
        - $ref: "#/components/schemas/PagamentoKamino"

    Produto:
      type: object
      properties:
        trinx_id:
          type: string
          example: "11653892"
        estabelecimento_id:
          type: string
          example: "272155"
        nome:
          type: string
          nullable: true
          example: "Ox 30vol Cream Developer 9% 1000ml"
        raw_data:
          type: object
          description: payload cru da Trinks, preservado inteiro
        synced_at:
          type: string
          format: date-time
          description: última sincronização do catálogo daquela unidade (semanal)

    VendaProduto:
      type: object
      properties:
        origem:
          type: string
          enum: [salao, loja]
          description: De qual das duas bases a linha veio.
          example: "loja"
        transacao_id:
          type: string
          example: "280010981"
        estabelecimento_id:
          type: string
          example: "272155"
        unidade_nome:
          type: string
          nullable: true
          description: Nome da unidade. Nulo se o estabelecimento não estiver cadastrado.
          example: "Loja Rebouças"
        data_hora:
          type: string
          format: date-time
          nullable: true
        cliente_id:
          type: string
          nullable: true
        cliente_nome:
          type: string
          nullable: true
        produto_id:
          type: string
          nullable: true
          example: "11653892"
        produto_nome:
          type: string
          nullable: true
          example: "Ox 30vol Cream Developer 9% 1000ml"
        quantidade:
          type: string
          nullable: true
          example: "1"
        valor_unitario:
          type: string
          nullable: true
          example: "0.06"
        valor_total:
          type: string
          nullable: true
          example: "0.06"
        profissional_id:
          type: string
          nullable: true
          description: quem realizou a venda; frequentemente nulo
        forma_pagamento:
          type: string
          nullable: true
          description: >
            Primeira forma de pagamento da transação. O valor "Descontar do Profissional"
            indica produto levado pelo profissional, e não venda ao cliente.
          example: "Descontar do Profissional"

    Profissional:
      type: object
      properties:
        id:
          type: integer
          example: 1
        nome:
          type: string
          example: "João Barbeiro"
        trinx_id:
          type: string
          nullable: true
          example: "PROF-111"
        trinx_estabelecimento_id:
          type: string
          nullable: true
          example: "12345"
        kamino_centro_custo_id:
          type: string
          nullable: true
          example: "CC-001"
        kamino_unidade_negocio_id:
          type: string
          nullable: true
          example: "UN-001"
        cpf:
          type: string
          nullable: true
          example: "123.456.789-00"
        ativo:
          type: boolean
          example: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Estabelecimento:
      type: object
      properties:
        id:
          type: integer
          example: 1
        nome:
          type: string
          example: "Studio W - Unidade Centro"
        trinx_id:
          type: string
          nullable: true
          example: "12345"
        kamino_id:
          type: string
          nullable: true
          example: "KAM-001"
        kamino_centro_custo_id:
          type: integer
          nullable: true
          example: 42
        ativo:
          type: boolean
          example: true
        created_at:
          type: string
          format: date-time

    JobLog:
      type: object
      properties:
        id:
          type: integer
          example: 1
        queue:
          type: string
          example: "trinx-sync"
        job_id:
          type: string
          nullable: true
          example: "job-abc123"
        job_name:
          type: string
          enum: [sync-transacoes, send-receber, send-pagar]
          example: "sync-transacoes"
        status:
          type: string
          enum: [started, completed, failed]
          example: "completed"
        error:
          type: string
          nullable: true
          description: Mensagem de erro (apenas em status=failed)
        duration_ms:
          type: integer
          nullable: true
          description: Duração da execução em milissegundos
          example: 1234
        created_at:
          type: string
          format: date-time

    Resumo:
      type: object
      properties:
        transacoes:
          type: object
          properties:
            total:
              type: integer
              example: 1543
            valor_total:
              type: number
              example: 87650.00
            enviadas_kamino:
              type: integer
              example: 1500
            pendentes_kamino:
              type: integer
              example: 43
        lancamentos:
          type: object
          properties:
            total:
              type: integer
              example: 320
        jobs_ultimas_24h:
          type: object
          properties:
            concluidos:
              type: integer
              example: 48
            falhados:
              type: integer
              example: 0

    TransacaoPorProfissional:
      type: object
      properties:
        profissional:
          type: string
          nullable: true
          example: "João Barbeiro"
        profissional_trinx_id:
          type: string
          nullable: true
          example: "PROF-111"
        estabelecimento_id:
          type: string
          example: "12345"
        total_transacoes:
          type: integer
          example: 85
        valor_total:
          type: number
          example: 12750.00

    TransacaoPorFormaPagamento:
      type: object
      properties:
        forma_pagamento:
          type: string
          nullable: true
          example: "Cartão de Crédito"
        total_transacoes:
          type: integer
          example: 650
        valor_total:
          type: number
          example: 55000.00
        ticket_medio:
          type: number
          example: 84.62
        taxa_cobrada_profissional:
          type: number
          nullable: true
          description: "Taxa decimal (ex: 0.0299 = 2.99%)"
          example: 0.0299
