openapi: 3.1.0
info:
  title: Levfone Connect — Partner PDV API
  version: 1.0.0
  summary: API implementada pelo PDV Parceiro e consumida pela Levfone.
  description: >
    # Partner PDV API


    Esta especificação define os endpoints que **o PDV Parceiro implementa** e
    que a

    **Levfone consome** durante a jornada de financiamento de smartphones no
    varejo.


    O fluxo inverso — a autorização de venda que o PDV Parceiro consome da
    Levfone —

    está especificado em `levfone-authorization-api.v1.yaml`.


    ## Direção de integração


    | Papel | Sistema |

    |---|---|

    | Provedor (implementa) | PDV Parceiro |

    | Consumidor (chama) | Levfone |


    ## Convenções


    - Todos os valores monetários trafegam como **inteiros em centavos**
    (`Money`).

    - Todas as respostas usam o envelope padrão Levfone Connect.

    - Todos os timestamps são **ISO 8601 em UTC** com milissegundos.

    - Toda requisição envia `X-Request-Id` e `X-Correlation-Id`.
  contact:
    name: Levfone Connect — Partner Integrations
    url: https://developer.levfone.com/support
    email: connect@levfone.com
  license:
    name: Proprietary — Levfone
    identifier: LicenseRef-Levfone-Proprietary
servers:
  - url: https://{host}/v1
    description: Ambiente do PDV Parceiro
    variables:
      host:
        default: sandbox.pdv-parceiro.example.com
        description: Host fornecido pelo PDV Parceiro (sandbox ou produção)
tags:
  - name: Authentication
    description: Autenticação sistema-a-sistema e sessão do vendedor.
  - name: Catalog
    description: Planos, produtos e serviços disponíveis na loja.
  - name: Inventory
    description: Validação de IMEI e disponibilidade em estoque.
  - name: Sales
    description: Consulta de status de venda, faturamento e cancelamento.
security:
  - partnerOAuth: []
paths:
  /oauth/token:
    post:
      tags:
        - Authentication
      operationId: issuePartnerToken
      summary: Emite token de acesso sistema-a-sistema
      description: |
        Autenticação **máquina-a-máquina** entre Levfone e PDV Parceiro,
        via OAuth 2.0 `client_credentials` (RFC 6749 §4.4).

        Este token identifica a **Levfone como aplicação**, não o vendedor.
        A identificação do vendedor é feita em `POST /sellers/session`.
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required:
                - grant_type
                - client_id
                - client_secret
              properties:
                grant_type:
                  type: string
                  const: client_credentials
                client_id:
                  type: string
                client_secret:
                  type: string
                  format: password
                  writeOnly: true
                scope:
                  type: string
                  examples:
                    - catalog:read inventory:read sales:read
      responses:
        '200':
          description: Token emitido
          content:
            application/json:
              schema:
                type: object
                required:
                  - access_token
                  - token_type
                  - expires_in
                properties:
                  access_token:
                    type: string
                  token_type:
                    type: string
                    const: Bearer
                  expires_in:
                    type: integer
                    examples:
                      - 3600
                  scope:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /sellers/session:
    post:
      tags:
        - Authentication
      operationId: createSellerSession
      summary: Identifica o vendedor e retorna o contexto de loja
      description: >
        Cria uma sessão de vendedor no PDV Parceiro.


        O `storeId` retornado é **obrigatório em todas as chamadas
        subsequentes** de

        catálogo, estoque e vendas.


        > **Nota de segurança.** Nesta versão a credencial do vendedor trafega
        pela

        > Levfone (*credential passthrough*). A Levfone **não persiste a senha**
        em

        > nenhuma hipótese — o campo é `writeOnly`, nunca é logado e é
        descartado

        > imediatamente após o encaminhamento. O caminho recomendado para a v2 é

        > **OIDC Authorization Code + PKCE hospedado pelo PDV Parceiro**,
        eliminando

        > o trânsito da senha. Ver ADR-001.
      parameters:
        - $ref: '#/components/parameters/XRequestId'
        - $ref: '#/components/parameters/XCorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - username
                - password
              properties:
                username:
                  type: string
                  maxLength: 120
                  examples:
                    - vendedor.1234
                password:
                  type: string
                  format: password
                  writeOnly: true
      responses:
        '200':
          description: Sessão criada
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/SellerSession'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /plans:
    get:
      tags:
        - Catalog
      operationId: listPlans
      summary: Lista os planos de operadora disponíveis na loja
      parameters:
        - $ref: '#/components/parameters/StoreIdQuery'
        - $ref: '#/components/parameters/XRequestId'
        - $ref: '#/components/parameters/XCorrelationId'
      responses:
        '200':
          description: Planos disponíveis
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          plans:
                            type: array
                            items:
                              $ref: '#/components/schemas/Plan'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /products:
    get:
      tags:
        - Catalog
      operationId: listProducts
      summary: Lista o catálogo de smartphones elegíveis ao plano
      description: >
        Retorna os smartphones disponíveis na loja para o plano informado.


        A cor **não** é retornada: a Levfone financia o SKU, e a variação de cor
        é

        resolvida no ato da venda pela leitura do IMEI. Ver ADR-006.
      parameters:
        - $ref: '#/components/parameters/StoreIdQuery'
        - name: planId
          in: query
          required: true
          schema:
            type: string
            maxLength: 64
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/XRequestId'
        - $ref: '#/components/parameters/XCorrelationId'
      responses:
        '200':
          description: Catálogo retornado
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          products:
                            type: array
                            items:
                              $ref: '#/components/schemas/Product'
                          pagination:
                            $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /products/barcode:
    get:
      tags:
        - Catalog
      operationId: getProductByBarcode
      summary: Consulta produto por código de barras
      description: |
        Endpoint universal de leitura de código de barras. Atende smartphones e
        **qualquer acessório** (capinha, película, cabo, carregador, fone).

        Garantias e seguros **não** são retornados aqui — use `GET /services`.
      parameters:
        - $ref: '#/components/parameters/StoreIdQuery'
        - name: barcode
          in: query
          required: true
          schema:
            type: string
            minLength: 6
            maxLength: 32
          examples:
            ean13:
              value: '7891234567895'
        - $ref: '#/components/parameters/XRequestId'
        - $ref: '#/components/parameters/XCorrelationId'
      responses:
        '200':
          description: Produto encontrado
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Product'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /services:
    get:
      tags:
        - Catalog
      operationId: listServices
      summary: Lista garantias, seguros e proteções aplicáveis a um SKU
      parameters:
        - $ref: '#/components/parameters/StoreIdQuery'
        - name: sku
          in: query
          required: true
          description: SKU do smartphone ao qual o serviço será vinculado.
          schema:
            type: string
            maxLength: 64
        - $ref: '#/components/parameters/XRequestId'
        - $ref: '#/components/parameters/XCorrelationId'
      responses:
        '200':
          description: Serviços disponíveis
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          services:
                            type: array
                            items:
                              $ref: '#/components/schemas/Service'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /imei/validate:
    get:
      tags:
        - Inventory
      operationId: validateImei
      summary: Resolve o IMEI para produto e indica disponibilidade
      description: >
        Resolve um IMEI lido pelo vendedor no aparelho SKU, marca, modelo,

        descrição e memória, e informa se está disponível na loja.


        O campo `message` é destinado à **exibição direta ao vendedor** e deve

        explicar o motivo da indisponibilidade em linguagem operacional.


        > **Este endpoint não é vinculante.** Ele não reserva o aparelho, não

        > garante disponibilidade e não decide nada. Serve à jornada da Levfone,

        > antes do carrinho existir, para saber qual produto está sendo

        > financiado.

        >

        > A única decisão sobre a venda é `POST /sales/authorize`, na

        > Sales Authorization API — uma chamada única e autocontida. Ver
        ADR-007.
      parameters:
        - $ref: '#/components/parameters/StoreIdQuery'
        - name: imei
          in: query
          required: true
          schema:
            type: string
            pattern: ^[0-9]{15}$
          examples:
            valid:
              value: '356938035643809'
        - $ref: '#/components/parameters/XRequestId'
        - $ref: '#/components/parameters/XCorrelationId'
      responses:
        '200':
          description: Resultado da validação
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/ImeiValidation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /sales/search:
    post:
      tags:
        - Sales
      operationId: searchSaleStatus
      summary: Consulta o status de faturamento e cancelamento de uma venda
      description: |
        Busca a venda por **CPF** ou **IMEI**. Exatamente um dos dois deve ser
        informado.

        Este endpoint é `POST` — e não `GET` — porque o CPF é dado pessoal e não
        pode trafegar em query string, onde seria persistido em logs de acesso,
        CDN e proxies. Ver ADR-004.

        A `nfeAccessKey` (chave de 44 dígitos) permite localizar e baixar a NF-e
        no portal da Receita Federal.
      parameters:
        - $ref: '#/components/parameters/XRequestId'
        - $ref: '#/components/parameters/XCorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              oneOf:
                - required:
                    - taxId
                - required:
                    - imei
              properties:
                taxId:
                  type: string
                  pattern: ^[0-9]{11}$
                  description: CPF sem máscara.
                imei:
                  type: string
                  pattern: ^[0-9]{15}$
      responses:
        '200':
          description: Vendas encontradas
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          sales:
                            type: array
                            items:
                              $ref: '#/components/schemas/SaleStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
components:
  securitySchemes:
    partnerOAuth:
      type: oauth2
      description: OAuth 2.0 client credentials emitido pelo PDV Parceiro.
      flows:
        clientCredentials:
          tokenUrl: https://sandbox.pdv-parceiro.example.com/v1/oauth/token
          scopes:
            catalog:read: Leitura de planos, produtos e serviços
            inventory:read: Validação de IMEI e estoque
            sales:read: Consulta de status de venda
  parameters:
    StoreIdQuery:
      name: storeId
      in: query
      required: true
      description: Identificador da loja, obtido em `POST /sellers/session`.
      schema:
        type: string
        maxLength: 64
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PageSize:
      name: pageSize
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    XRequestId:
      name: X-Request-Id
      in: header
      required: true
      description: UUID v4 único por requisição.
      schema:
        type: string
        format: uuid
    XCorrelationId:
      name: X-Correlation-Id
      in: header
      required: true
      description: UUID v4 que agrupa todas as chamadas de uma mesma jornada de venda.
      schema:
        type: string
        format: uuid
  schemas:
    Envelope:
      type: object
      required:
        - success
        - message
        - timestamp
      properties:
        success:
          type: boolean
        message:
          type: string
          description: >-
            Mensagem apta a ser exibida ao vendedor. Vazia em sucesso
            silencioso.
        timestamp:
          type: string
          format: date-time
        requestId:
          type: string
          format: uuid
        data:
          type: object
        error:
          oneOf:
            - $ref: '#/components/schemas/Error'
            - type: 'null'
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: >
            Código estável e semântico, em `snake_case`. Nunca muda entre
            versões.
          examples:
            - store_not_found
            - imei_not_in_stock
        legacyCode:
          type: string
          pattern: ^LF[0-9]{4}$
          description: Alias numérico para abertura de chamado no suporte.
          examples:
            - LF0042
        message:
          type: string
        details:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              issue:
                type: string
        docsUrl:
          type: string
          format: uri
    Money:
      type: object
      required:
        - amount
        - currency
      description: |
        Valor monetário em **unidade menor** (centavos), sempre inteiro.
        Ponto flutuante é proibido em toda a plataforma. Ver ADR-003.
      properties:
        amount:
          type: integer
          description: Valor em centavos.
          examples:
            - 129900
        currency:
          type: string
          const: BRL
    Pagination:
      type: object
      properties:
        page:
          type: integer
        pageSize:
          type: integer
        totalItems:
          type: integer
        totalPages:
          type: integer
    SellerSession:
      type: object
      required:
        - sellerId
        - sellerName
        - taxId
        - storeId
        - storeName
        - storeCnpj
        - storeAddress
        - accessToken
        - expiresAt
      properties:
        sellerId:
          type: string
        sellerName:
          type: string
        taxId:
          type: string
          pattern: ^[0-9]{11}$
          description: |
            CPF do vendedor, sem máscara. Usado pela Levfone para vincular a
            sessão ao cadastro interno do vendedor.
        storeId:
          type: string
        storeName:
          type: string
        storeCnpj:
          type: string
          pattern: ^[0-9]{14}$
          description: CNPJ da loja, sem máscara.
        storeAddress:
          $ref: '#/components/schemas/Address'
        role:
          type: string
          examples:
            - SELLER
            - SUPERVISOR
            - MANAGER
        accessToken:
          type: string
          description: Token de sessão do vendedor, escopado à loja.
        expiresAt:
          type: string
          format: date-time
    Address:
      type: object
      required:
        - street
        - city
        - state
        - zipCode
      properties:
        street:
          type: string
          description: Logradouro completo, incluindo número e complemento.
          examples:
            - Av. Paulista, 1000, Sala 12
        city:
          type: string
          examples:
            - São Paulo
        state:
          type: string
          pattern: ^[A-Z]{2}$
          description: UF, duas letras maiúsculas.
          examples:
            - SP
        zipCode:
          type: string
          pattern: ^[0-9]{8}$
          description: CEP sem máscara e sem hífen.
          examples:
            - '01310100'
    Plan:
      type: object
      required:
        - planId
        - name
        - monthlyPrice
      properties:
        planId:
          type: string
        name:
          type: string
          examples:
            - Controle 25GB
        monthlyPrice:
          $ref: '#/components/schemas/Money'
    Product:
      type: object
      required:
        - sku
        - name
        - price
        - stock
      properties:
        sku:
          type: string
        productType:
          type: string
          enum:
            - SMARTPHONE
            - ACCESSORY
        name:
          type: string
        brand:
          type: string
        model:
          type: string
        description:
          type: string
        storage:
          type: string
          description: Capacidade de armazenamento, com unidade.
          examples:
            - 128GB
        price:
          $ref: '#/components/schemas/Money'
        stock:
          type: integer
          minimum: 0
    Service:
      type: object
      required:
        - serviceId
        - name
        - type
        - price
      properties:
        serviceId:
          type: string
        name:
          type: string
          examples:
            - Garantia Estendida 12 meses
        description:
          type: string
        type:
          type: string
          enum:
            - EXTENDED_WARRANTY
            - INSURANCE
            - PROTECTION
        price:
          $ref: '#/components/schemas/Money'
    ImeiValidation:
      type: object
      required:
        - imeiValid
        - inStock
      properties:
        imeiValid:
          type: boolean
        inStock:
          type: boolean
        sku:
          type: string
        name:
          type: string
        brand:
          type: string
        model:
          type: string
        description:
          type: string
        storage:
          type: string
        message:
          type: string
          description: Texto a ser exibido ao vendedor explicando a reprovação.
          examples:
            - IMEI pertence a outra loja.
    SaleStatus:
      type: object
      required:
        - status
        - saleNumber
        - saleDate
      properties:
        status:
          type: string
          enum:
            - PENDING
            - ACTIVE
            - CANCELLED
          description: |
            - `PENDING` — venda iniciada, ainda não faturada.
            - `ACTIVE` — venda faturada, NF-e emitida.
            - `CANCELLED` — venda cancelada e estoque liberado no PDV.
        saleNumber:
          type: string
        orderNumber:
          type: string
        invoiceNumber:
          type: string
        nfeAccessKey:
          type: string
          pattern: ^[0-9]{44}$
          description: Chave de acesso da NF-e (44 dígitos).
        saleDate:
          type: string
          format: date-time
        cancellationDate:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        message:
          type: string
  responses:
    Unauthorized:
      description: Token ausente, inválido ou expirado.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Envelope'
    NotFound:
      description: Recurso não encontrado.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Envelope'
    UnprocessableEntity:
      description: Requisição sintaticamente válida, mas semanticamente inválida.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Envelope'
    TooManyRequests:
      description: Limite de requisições excedido.
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Envelope'
