openapi: 3.0.3

info:
  title: Club Fidelidade — API de Integração ERP
  version: 1.1.0
  description: |
    API para integração entre o ERP **Infinite** e o sistema de fidelidade **Club Fidelidade**.

    ## Visão Geral

    Ao finalizar uma venda, o ERP chama este endpoint com três campos:
    o CPF do cliente, o nome do frentista e o valor da compra.
    O Club Fidelidade processa tudo automaticamente:

    | Situação | Comportamento |
    |---|---|
    | CPF cadastrado no clube | Pontos creditados instantaneamente |
    | CPF **não** cadastrado | Lead registrado; dono do posto envia convite |
    | Frentista novo | Cadastro automático com ID gerado pelo sistema |

    ## Autenticação

    Cada posto recebe uma **chave de API** única gerada no painel administrativo.
    Ela deve ser enviada no header `Authorization` como Bearer token:

    ```
    Authorization: Bearer <chave-do-posto>
    ```

    > A chave é exibida apenas no momento da criação — guarde-a em local seguro.
    > Em caso de comprometimento, solicite a revogação ao administrador.

    ## Ambiente de Testes

    Para homologação, utilize o servidor `http://localhost:54321/functions/v1`
    com o Supabase CLI em execução local e uma chave de teste cadastrada no banco.

  contact:
    name: Suporte Club Fidelidade
    email: suporte@clubfidelidade.com.br

  license:
    name: Proprietário — uso restrito à integração com Club Fidelidade

servers:
  - url: https://{project_ref}.supabase.co/functions/v1
    description: Produção
    variables:
      project_ref:
        description: Referência do projeto Supabase (fornecida pelo Club Fidelidade)
        default: club-fidelidade
  - url: http://localhost:54321/functions/v1
    description: Homologação / desenvolvimento local

tags:
  - name: Compras
    description: Registro de compras e acúmulo de pontos de fidelidade
  - name: Clientes
    description: Consulta de cliente e saldo de pontos pelo PDV
  - name: Resgate
    description: Confirmação de resgate de prêmios pela maquininha/PDV

paths:

  /erp-cliente:
    get:
      tags:
        - Clientes
      summary: Consultar cliente por CPF
      operationId: consultarCliente
      description: |
        Consulta o nome e o saldo de pontos de um cliente pelo CPF.
        Usada pelo PDV/maquininha antes do resgate, para o frentista
        confirmar com o cliente o saldo disponível.

        Também aceita o parâmetro `customerCode` como alias de `cpf`
        (compatibilidade com módulos de fidelidade de PDV).
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - name: cpf
          in: query
          required: true
          description: CPF do cliente, com ou sem formatação.
          schema:
            type: string
            example: "123.456.789-09"
      responses:
        '200':
          description: Cliente encontrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClienteResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: CPF não cadastrado no clube deste posto.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'
              example:
                erro: "Cliente não encontrado para este CPF"
        '500':
          $ref: '#/components/responses/InternalError'

  /erp-resgate:
    post:
      tags:
        - Resgate
      summary: Confirmar resgate por código
      operationId: confirmarResgate
      description: |
        Confirma a entrega de um resgate. O cliente gera um **código de
        6 dígitos** no app, informa CPF + código ao frentista, e o PDV
        chama este endpoint. A resposta é **síncrona** e traz os itens
        do prêmio para exibição/impressão no comprovante.

        Os pontos já foram debitados quando o cliente criou o pedido no
        app — este endpoint apenas valida o código e registra a entrega.
        O código é de **uso único**: a segunda chamada com o mesmo código
        retorna 404.

        Aceita também os nomes de campo `customerDocument`, `voucherCode`
        e `collaborator` como aliases (compatibilidade com módulos de
        fidelidade de PDV).
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResgateRequest'
            examples:
              resgate:
                summary: Resgate informado no balcão
                value:
                  cpf: "123.456.789-09"
                  codigo: "042137"
                  frentista_nome: "João Silva"
      responses:
        '200':
          description: Resgate confirmado — entregar o prêmio ao cliente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResgateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: CPF não cadastrado, ou código inexistente / já utilizado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'
              example:
                erro: "Código não encontrado ou já utilizado"
        '409':
          description: O código existe, mas pertence a outro cliente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'
              example:
                erro: "Código não pertence a este cliente"
        '500':
          $ref: '#/components/responses/InternalError'

  /erp-webhook:
    post:
      tags:
        - Compras
      summary: Registrar compra
      operationId: registrarCompra
      description: |
        Endpoint principal de integração. **Deve ser chamado ao finalizar cada venda.**

        ### Fluxo de processamento

        ```
        ERP envia POST
            │
            ├─ Valida chave de API do posto
            ├─ Encontra ou cria frentista pelo nome → gera attendant_id
            ├─ Busca cliente pelo CPF
            │       │
            │       ├─ Cadastrado ──▶ Calcula pontos → Credita → Retorna saldo
            │       └─ Não cadastrado ──▶ Cria lead → Retorna mensagem
            └─ Grava auditoria da compra (sempre)
        ```

        ### Pontuação

        Os pontos são calculados com base na regra configurada no programa de
        fidelidade do posto: `pontos = floor(purchase_value × pontos_por_real)`,
        com mínimo de 1 ponto por compra.

        ### Retry e idempotência

        Não há suporte nativo a idempotência nesta versão. Em caso de timeout,
        recomenda-se aguardar ao menos **5 segundos** antes de retentar.
        Retries devem ser limitados a **2 tentativas** para evitar crédito duplo.

      security:
        - BearerAuth: []

      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompraRequest'
            examples:
              cliente_cadastrado:
                summary: Cliente cadastrado no clube
                value:
                  customer_cpf: "123.456.789-09"
                  attendant_name: "João Silva"
                  purchase_value: 150.00
              cliente_nao_cadastrado:
                summary: Cliente ainda não cadastrado
                value:
                  customer_cpf: "987.654.321-00"
                  attendant_name: "Maria Santos"
                  purchase_value: 80.50
              cpf_sem_mascara:
                summary: CPF sem formatação (também aceito)
                value:
                  customer_cpf: "12345678909"
                  attendant_name: "Pedro Lima"
                  purchase_value: 200.00

      responses:
        '200':
          description: Compra registrada com sucesso
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/RespostaCadastrado'
                  - $ref: '#/components/schemas/RespostaNaoCadastrado'
                discriminator:
                  propertyName: cliente_cadastrado
              examples:
                pontos_creditados:
                  summary: Cliente cadastrado — pontos creditados
                  value:
                    sucesso: true
                    cliente_cadastrado: true
                    cliente_nome: "Maria Souza"
                    pontos_ganhos: 150
                    pontos_totais: 820
                lead_criado:
                  summary: Cliente não cadastrado — lead criado
                  value:
                    sucesso: true
                    cliente_cadastrado: false
                    mensagem: "CPF não cadastrado. Lead criado para envio de convite."

        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '500':
          $ref: '#/components/responses/InternalError'

components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Chave de API única por posto. Gerada e gerenciada no painel administrativo
        do Club Fidelidade. Solicitar ao administrador do sistema.
    ApiKeyAuth:
      type: apiKey
      in: header
      name: api_key
      description: |
        Alternativa ao Bearer token: a mesma chave do posto enviada no header
        `api_key`. Disponível nos endpoints `/erp-cliente` e `/erp-resgate`
        (compatibilidade com módulos de fidelidade de PDV).

  schemas:

    CompraRequest:
      type: object
      required:
        - customer_cpf
        - attendant_name
        - purchase_value
      properties:
        customer_cpf:
          type: string
          description: |
            CPF do cliente. Aceita com ou sem formatação (pontos e hífen).
            Validado pelos dois dígitos verificadores.
          example: "123.456.789-09"
          pattern: '^\d{3}\.?\d{3}\.?\d{3}-?\d{2}$'
        attendant_name:
          type: string
          description: |
            Nome completo do frentista conforme registrado no ERP.
            Se for a primeira ocorrência do nome, um `attendant_id` será gerado
            automaticamente no Club Fidelidade.
          example: "João Silva"
          minLength: 1
          maxLength: 255
        purchase_value:
          type: number
          format: float
          description: Valor total da compra em reais (R$).
          example: 150.00
          minimum: 0.01
          exclusiveMinimum: true

    RespostaCadastrado:
      type: object
      description: Retornado quando o CPF está cadastrado no clube e pontos foram creditados.
      required:
        - sucesso
        - cliente_cadastrado
        - cliente_nome
        - pontos_ganhos
        - pontos_totais
      properties:
        sucesso:
          type: boolean
          example: true
        cliente_cadastrado:
          type: boolean
          example: true
        cliente_nome:
          type: string
          description: Nome completo do cliente conforme cadastro no clube.
          example: "Maria Souza"
        pontos_ganhos:
          type: integer
          description: Pontos creditados nesta transação.
          example: 150
        pontos_totais:
          type: integer
          description: Saldo total de pontos do cliente após esta compra.
          example: 820

    RespostaNaoCadastrado:
      type: object
      description: |
        Retornado quando o CPF não está cadastrado no clube.
        A compra é registrada no log de auditoria e o CPF fica disponível
        no painel do posto para envio de convite.
      required:
        - sucesso
        - cliente_cadastrado
        - mensagem
      properties:
        sucesso:
          type: boolean
          example: true
        cliente_cadastrado:
          type: boolean
          example: false
        mensagem:
          type: string
          example: "CPF não cadastrado. Lead criado para envio de convite."

    ClienteResponse:
      type: object
      required:
        - nome
        - cpf
        - pontos
      properties:
        nome:
          type: string
          description: Nome completo do cliente conforme cadastro no clube.
          example: "Maria Souza"
        cpf:
          type: string
          description: CPF do cliente sem formatação.
          example: "12345678909"
        pontos:
          type: integer
          description: Saldo de pontos disponível para resgate.
          example: 540

    ResgateRequest:
      type: object
      required:
        - cpf
        - codigo
        - frentista_nome
      properties:
        cpf:
          type: string
          description: CPF do cliente, com ou sem formatação. Alias aceito — `customerDocument`.
          example: "123.456.789-09"
        codigo:
          type: string
          description: Código de resgate de 6 dígitos gerado no app do cliente. Alias aceito — `voucherCode`.
          example: "042137"
          pattern: '^\d{6}$'
        frentista_nome:
          type: string
          description: Nome do frentista/operador que atendeu, para auditoria. Alias aceito — `collaborator`.
          example: "João Silva"

    ResgateResponse:
      type: object
      required:
        - sucesso
        - pedido_id
        - cliente_nome
        - pontos_total
        - itens
        - saldo_pontos
      properties:
        sucesso:
          type: boolean
          example: true
        pedido_id:
          type: string
          format: uuid
          description: Identificador do pedido de resgate concluído.
        cliente_nome:
          type: string
          example: "Maria Souza"
        cliente_cpf:
          type: string
          example: "12345678909"
        pontos_total:
          type: integer
          description: Pontos consumidos por este resgate.
          example: 120
        itens:
          type: array
          description: Itens do prêmio, para exibição/impressão no PDV.
          items:
            type: object
        saldo_pontos:
          type: integer
          description: Saldo de pontos restante do cliente.
          example: 420
        concluido_em:
          type: string
          format: date-time

    Erro:
      type: object
      required:
        - erro
      properties:
        erro:
          type: string
          description: Descrição legível do erro.
          example: "Mensagem descritiva do erro"

  responses:

    BadRequest:
      description: Dados inválidos ou campos obrigatórios ausentes.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Erro'
          examples:
            campos_ausentes:
              summary: Campo obrigatório não enviado
              value:
                erro: "Campos obrigatórios: customer_cpf, attendant_name, purchase_value"
            cpf_invalido:
              summary: CPF com dígito verificador inválido
              value:
                erro: "customer_cpf inválido"
            valor_invalido:
              summary: Valor zerado ou negativo
              value:
                erro: "purchase_value deve ser um número positivo"
            attendant_invalido:
              summary: Nome do frentista vazio
              value:
                erro: "attendant_name inválido"

    Unauthorized:
      description: Chave de API ausente, inválida ou desativada.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Erro'
          example:
            erro: "Não autorizado"

    NotFound:
      description: Programa de fidelidade ativo não encontrado para o posto.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Erro'
          example:
            erro: "Programa de fidelidade ativo não encontrado"

    MethodNotAllowed:
      description: Apenas o método POST é aceito neste endpoint.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Erro'
          example:
            erro: "Método não permitido"

    InternalError:
      description: Erro interno do servidor. Contatar o suporte do Club Fidelidade.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Erro'
          example:
            erro: "Erro interno do servidor"
