openapi: 3.1.0

info:
  title: Levfone Connect — Sales Authorization API
  version: "1.0.0"
  summary: API implementada pela Levfone e consumida pelo PDV Parceiro.
  description: |
    # Sales Authorization API

    Esta é a **única** API do Levfone Connect implementada pela Levfone. O PDV
    Parceiro a consome no momento em que o vendedor seleciona **Levfone** como
    forma de pagamento.

    ## Direção de integração

    | Papel | Sistema |
    |---|---|
    | Provedor (implementa) | Levfone |
    | Consumidor (chama) | PDV Parceiro |

    ## Ciclo de vida da autorização

    ```
    authorize  ──►  AUTHORIZED (com TTL)  ──►  confirm  ──►  CONFIRMED
                          │                                      │
                          ├──► expira (TTL)  ──► EXPIRED         └──► cancel ──► CANCELLED
                          └──► cancel        ──► CANCELLED
    ```

    O `authorizationId` **expira**. Se o PDV Parceiro não confirmar o faturamento
    dentro do TTL, a autorização é invalidada e a proposta de crédito volta a ficar
    disponível. O TTL padrão é de **15 minutos** e vem em `expiresAt`.

    ## Idempotência

    `POST /sales/authorize` **exige** o header `Idempotency-Key`. Repetir a mesma
    chave dentro de 24 horas retorna a resposta original sem reprocessar — protege
    contra timeout de rede, retry automático e duplo clique do vendedor.

  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://api.sandbox.levfone.com/v1
    description: Sandbox
  - url: https://api.levfone.com/v1
    description: Produção

tags:
  - name: Authentication
    description: Emissão de token de acesso para o PDV Parceiro.
  - name: Authorization
    description: Autorização, confirmação e cancelamento de venda financiada.

security:
  - levfoneOAuth: []

paths:

  /oauth/token:
    post:
      tags: [Authentication]
      operationId: issueLevfoneToken
      summary: Emite token de acesso para o PDV Parceiro
      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: ["sales:authorize"] }
      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" }

  /sales/authorize:
    post:
      tags: [Authorization]
      operationId: authorizeSale
      summary: Autoriza uma venda financiada pela Levfone
      description: |
        Chamada pelo PDV Parceiro **após** o carrinho estar montado e a forma de
        pagamento Levfone selecionada.

        ### Chamada única e autocontida

        Não há passo prévio obrigatório. O PDV pede a autorização e recebe ou a
        autorização ou o motivo da recusa, em uma única ida e volta.

        A Levfone **não** consulta o PDV Parceiro durante o processamento desta
        chamada. O PDV é a fonte da verdade sobre estoque e é ele quem está
        afirmando o IMEI nesta requisição — reconsultá-lo seria circular. A
        validação ocorre contra os registros da própria Levfone. Ver ADR-007.

        ### Ordem de validação

        1. Existe proposta **aprovada e vigente** para o CPF na loja informada,
           ou proposta reaproveitável por troca (ver ADR-008).
        2. O `planId` corresponde ao plano da proposta.
        3. Os produtos e serviços estão contemplados pela proposta.
        4. O **valor total** confere com o valor aprovado.
        5. O IMEI tem formato válido e não está vinculado a outro contrato ativo.

        **O PDV Parceiro só pode concluir o faturamento após `authorized: true`.**

        ### Troca

        Se esta requisição for a nova venda de uma troca, a proposta original é
        reaproveitada **sem nova análise de crédito**. A Levfone infere a troca —
        o PDV Parceiro não envia nada diferente e não muda nada no seu fluxo.
        Condições em ADR-008.

        ### Reconciliação de valores

        O campo `totals.grandTotal` deve ser **exatamente** a soma de
        `items[].totalPrice` + `services[].price`. Divergência de qualquer centavo
        retorna `total_mismatch` — a Levfone não arredonda nem ajusta.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/XRequestId"
        - $ref: "#/components/parameters/XCorrelationId"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AuthorizationRequest" }
      responses:
        "200":
          description: |
            Requisição processada. Verifique `data.authorized` — uma **recusa de
            crédito é `200`**, não erro. Códigos 4xx indicam problema na requisição.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/AuthorizationResult" }
              examples:
                aprovada:
                  summary: Venda autorizada
                  value:
                    success: true
                    message: "Venda autorizada."
                    timestamp: "2026-07-23T14:32:10.412Z"
                    data:
                      authorized: true
                      authorizationId: "auth_01J8XK4T7ZQ9"
                      status: AUTHORIZED
                      expiresAt: "2026-07-23T14:47:10.412Z"
                      message: "Venda autorizada. Conclua o faturamento."
                    error: null
                recusada:
                  summary: Venda não autorizada
                  value:
                    success: true
                    message: "Venda não autorizada."
                    timestamp: "2026-07-23T14:32:10.412Z"
                    data:
                      authorized: false
                      authorizationId: null
                      status: DECLINED
                      declineReason: total_mismatch
                      message: "Valor do carrinho difere do valor aprovado na proposta."
                    error: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409":
          description: |
            `Idempotency-Key` reutilizada com corpo de requisição diferente.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /sales/{authorizationId}/confirm:
    post:
      tags: [Authorization]
      operationId: confirmSale
      summary: Confirma o faturamento da venda autorizada
      description: |
        Notifica a Levfone de que a venda foi **efetivamente faturada** no PDV
        Parceiro, encerrando o ciclo da autorização.

        Este endpoint é **recomendado, não obrigatório**. Se o PDV Parceiro não o
        implementar, a Levfone recorre ao polling de `POST /sales/search` na
        Partner PDV API. A confirmação ativa é preferível: elimina a janela em que
        o crédito está comprometido sem que a Levfone saiba o desfecho, e reduz
        drasticamente o volume de polling. Ver ADR-005.
      parameters:
        - $ref: "#/components/parameters/AuthorizationIdPath"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/XRequestId"
        - $ref: "#/components/parameters/XCorrelationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [saleNumber, invoiceNumber, nfeAccessKey, invoicedAt]
              properties:
                saleNumber: { type: string }
                orderNumber: { type: string }
                invoiceNumber: { type: string }
                nfeAccessKey:
                  type: string
                  pattern: "^[0-9]{44}$"
                invoicedAt: { type: string, format: date-time }
      responses:
        "200":
          description: Faturamento registrado
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          authorizationId: { type: string }
                          status: { type: string, const: CONFIRMED }
                          contractId:
                            type: string
                            description: Identificador do crediário formalizado.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Autorização expirada ou já em estado terminal.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }

  /sales/{authorizationId}/cancel:
    post:
      tags: [Authorization]
      operationId: cancelSale
      summary: Notifica o cancelamento de uma venda
      description: |
        Notifica a Levfone de que a venda foi cancelada no PDV Parceiro.

        **A Levfone nunca inicia um cancelamento.** O gatilho é sempre o vendedor,
        no PDV. Este endpoint apenas comunica um cancelamento **já efetivado** —
        pedido cancelado, nota cancelada e estoque liberado.

        A Levfone confirma o cancelamento contra `POST /sales/search` antes de
        cancelar o financiamento. Divergência entre a notificação e a consulta faz
        prevalecer **a consulta**.

        ### Troca

        Não existe endpoint de troca. Se o PDV Parceiro trata troca como
        cancelamento + nova venda, a Levfone faz o mesmo: `cancel` seguido de novo
        `authorize`.

        O valor `reason: EXCHANGE` é o **gatilho do reaproveitamento da proposta**.
        Cancelamento marcado como troca permite que o novo `authorize` reutilize a
        aprovação original sem nova análise de crédito, dentro dos limites do
        ADR-008. Qualquer outro `reason` encerra a proposta.

        Se o PDV Parceiro não distingue troca de cancelamento comum, envie
        `EXCHANGE` sempre que houver venda subsequente ao cliente na mesma visita.
      parameters:
        - $ref: "#/components/parameters/AuthorizationIdPath"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/XRequestId"
        - $ref: "#/components/parameters/XCorrelationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cancelledAt, reason]
              properties:
                cancelledAt: { type: string, format: date-time }
                reason:
                  type: string
                  enum: [CUSTOMER_REQUEST, EXCHANGE, OPERATIONAL_ERROR, FRAUD_SUSPICION, OTHER]
                notes: { type: string, maxLength: 500 }
      responses:
        "200":
          description: Cancelamento registrado
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          authorizationId: { type: string }
                          status: { type: string, const: CANCELLED }
                          financingCancelled:
                            type: boolean
                            description: |
                              `false` enquanto a Levfone ainda não confirmou o
                              cancelamento via `POST /sales/search`.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

components:

  securitySchemes:
    levfoneOAuth:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://api.sandbox.levfone.com/v1/oauth/token
          scopes:
            "sales:authorize": Autorizar, confirmar e cancelar vendas financiadas

  parameters:
    AuthorizationIdPath:
      name: authorizationId
      in: path
      required: true
      schema: { type: string, maxLength: 64 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        UUID v4 gerado pelo PDV Parceiro. Retenção de 24 horas.
      schema: { type: string, format: uuid }
    XRequestId:
      name: X-Request-Id
      in: header
      required: true
      schema: { type: string, format: uuid }
    XCorrelationId:
      name: X-Correlation-Id
      in: header
      required: true
      schema: { type: string, format: uuid }

  schemas:

    Envelope:
      type: object
      required: [success, message, timestamp]
      properties:
        success: { type: boolean }
        message: { type: string }
        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, examples: ["proposal_not_found"] }
        legacyCode: { type: string, pattern: "^LF[0-9]{4}$" }
        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]
      properties:
        amount: { type: integer, description: Valor em centavos. }
        currency: { type: string, const: BRL }

    AuthorizationRequest:
      type: object
      required: [taxId, storeId, planId, imei, items, totals]
      properties:
        taxId:
          type: string
          pattern: "^[0-9]{11}$"
          description: CPF do cliente, sem máscara.
        storeId: { type: string }
        sellerId: { type: string }
        planId: { type: string }
        imei:
          type: string
          pattern: "^[0-9]{15}$"
          description: IMEI do smartphone financiado.
        items:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/CartItem" }
        services:
          type: array
          items: { $ref: "#/components/schemas/CartService" }
        totals: { $ref: "#/components/schemas/CartTotals" }

    CartItem:
      type: object
      required: [sku, quantity, unitPrice, totalPrice]
      properties:
        sku: { type: string }
        productType: { type: string, enum: [SMARTPHONE, ACCESSORY] }
        name: { type: string }
        quantity: { type: integer, minimum: 1 }
        unitPrice: { $ref: "#/components/schemas/Money" }
        totalPrice: { $ref: "#/components/schemas/Money" }

    CartService:
      type: object
      required: [serviceId, price]
      properties:
        serviceId: { type: string }
        name: { type: string }
        type: { type: string, enum: [EXTENDED_WARRANTY, INSURANCE, PROTECTION] }
        price: { $ref: "#/components/schemas/Money" }

    CartTotals:
      type: object
      required: [productsTotal, grandTotal]
      properties:
        productsTotal: { $ref: "#/components/schemas/Money" }
        servicesTotal: { $ref: "#/components/schemas/Money" }
        discountTotal: { $ref: "#/components/schemas/Money" }
        grandTotal: { $ref: "#/components/schemas/Money" }

    AuthorizationResult:
      type: object
      required: [authorized, status, message]
      properties:
        authorized: { type: boolean }
        authorizationId:
          oneOf:
            - type: string
            - type: "null"
          description: Presente apenas quando `authorized` é `true`.
        status:
          type: string
          enum: [AUTHORIZED, DECLINED, CONFIRMED, CANCELLED, EXPIRED]
        expiresAt:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        declineReason:
          type: string
          description: Código semântico do motivo da recusa.
          examples: ["proposal_not_found", "total_mismatch", "imei_already_financed"]
        message:
          type: string
          description: Texto pronto para exibição ao vendedor.

  responses:
    Unauthorized:
      description: Token ausente, inválido ou expirado.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Envelope" }
    NotFound:
      description: Autorização não encontrada.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Envelope" }
    UnprocessableEntity:
      description: Requisição 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" }
