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" }
