openapi: 3.0.3
info:
  title: Nomli — API de Integração Externa
  description: |
    API REST para integração de sistemas terceiros (ERPs, PDVs) com a plataforma Nomli.

    ## Autenticação
    Todas as requisições (exceto `/docs` e `/openapi`) exigem o header:
    ```
    Authorization: Bearer <seu_token>
    ```
    O token é gerado no painel do restaurante em **Integrações → Tokens** ou pelo gestor da plataforma.

    ## Rate Limit
    - Intervalo mínimo configurável por token (padrão: **30 segundos** entre requisições)
    - **5 violações** dentro de 10 minutos → token bloqueado automaticamente
    - Bloqueio revertido somente pelo gestor da plataforma
    - Respostas com status `429` incluem `retry_after` (segundos para aguardar)

    As rotas `/catalog/*` **não** usam o intervalo mínimo — enviar um catálogo
    inteiro bloquearia o token e derrubaria junto o recebimento de pedidos. Elas
    têm limite próprio de **60 requisições por minuto**.

    ## Fluxo recomendado de polling
    1. Chamar `GET /api/ext/events/polling` a cada N segundos (respeitando o intervalo mínimo)
    2. Para cada evento recebido, chamar `GET /api/ext/orders/{id}` para obter os detalhes
    3. Processar o pedido no sistema local
    4. Chamar `POST /api/ext/events/acknowledgment` com os IDs confirmados
    5. Eventos não confirmados continuam aparecendo no próximo polling

    ## Códigos de evento
    | Código | Significado |
    |--------|-------------|
    | `PLC`  | Novo pedido recebido |
    | `CFM`  | Pedido aceito pelo restaurante |
    | `DSP`  | Pedido despachado para entrega |
    | `RTP`  | Pedido pronto para retirada no balcão |
    | `CON`  | Pedido concluído/entregue |
    | `CAN`  | Pedido cancelado |

    ## Envio de catálogo
    1. `POST /api/ext/catalog/sync` com o catálogo **completo**
    2. A resposta traz `imagens_pendentes` — os produtos cuja imagem mudou
    3. `POST /api/ext/catalog/image` para cada um deles

    O envio é sempre completo: produto ou grupo que não vier é **desativado**
    (nunca excluído, porque pedidos antigos referenciam esses registros). Um
    catálogo parcial derruba o cardápio da loja.

    Problemas que não impedem a carga — grupo inexistente, rótulo de tamanho sem
    correspondência — voltam em `avisos` na resposta `200`, e o item é ignorado.

  version: "1.0.0"
  contact:
    name: Nomli — Suporte Técnico

servers:
  - url: /api/ext
    description: Produção

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Token

  schemas:
    Evento:
      type: object
      properties:
        id:
          type: integer
          example: 42
        code:
          type: string
          enum: [PLC, CFM, DSP, RTP, CON, CAN]
          example: PLC
        nr_orcamen:
          type: integer
          example: 88
        created_at:
          type: string
          example: "2026-06-18 14:32:00"

    Pedido:
      type: object
      properties:
        nr_orcamen:
          type: integer
        nm_statuss:
          type: string
          example: pendente
        tp_orcamen:
          type: string
          example: VEN
        fl_entbalc:
          type: string
          enum: [D, B]
          description: "D = Delivery, B = Balcão"
        nm_cliente:
          type: string
        ds_observa:
          type: string
        tp_esperar:
          type: integer
          description: Tempo estimado em minutos
        dt_inclusa:
          type: string
          format: date
        hr_inclusa:
          type: string
        totais:
          type: object
          properties:
            vr_produto: { type: number }
            vr_fretess: { type: number }
            vr_descont: { type: number }
            vr_liquido: { type: number }
        endereco:
          nullable: true
          type: object
          properties:
            logradouro: { type: string }
            numero:     { type: string }
            complemento:{ type: string }
            referencia: { type: string }
            bairro:     { type: string }
            cidade:     { type: string }
        pagamento:
          type: object
          properties:
            ds_formpag: { type: string }
            tp_formpag: { type: string }
            vr_parcela: { type: number }
        itens:
          type: array
          items:
            $ref: '#/components/schemas/ItemPedido'

    ItemPedido:
      type: object
      properties:
        nr_sequenc:
          type: integer
        cd_produto:
          type: integer
        cd_pdvext:
          type: string
          nullable: true
          description: Código do produto no sistema externo (PDV/ERP)
          example: "4521"
        ds_produto:
          type: string
        qt_produto:
          type: integer
        vr_produto:
          type: number
        vr_totprod:
          type: number
        vr_liquido:
          type: number
        ds_tamanho:
          type: string
          nullable: true
        ds_massass:
          type: string
          nullable: true
          description: Tipo de massa (pizza)
        vr_massass:
          type: number
          nullable: true
        ds_bordass:
          type: string
          nullable: true
          description: Tipo de borda (pizza)
        vr_bordass:
          type: number
          nullable: true
        ds_opcoess:
          type: string
          nullable: true
          description: Opções/personalizações selecionadas (blocos de personalização)
        ds_observa:
          type: string
          nullable: true

    MerchantStatus:
      type: object
      properties:
        ds_fantasi:
          type: string
        fl_entrega:
          type: boolean
        fl_balcoes:
          type: boolean

    HorarioDia:
      type: object
      properties:
        dia_semana:
          type: integer
          description: "0=Domingo, 1=Segunda, ..., 6=Sábado"
        ds_dia:
          type: string
          example: segunda
        aberto:
          type: boolean
        janelas:
          type: array
          items:
            type: object
            properties:
              abertura:
                type: string
                example: "11:00"
              fechamento:
                type: string
                example: "22:00"

    ErroRateLimit:
      type: object
      properties:
        message:
          type: string
          example: "Aguarde pelo menos 30 segundos entre requisições."
        retry_after:
          type: integer
          description: Segundos para aguardar antes de tentar novamente
          example: 22
        violation:
          type: integer
          description: Número da violação atual (bloqueio ocorre na 5ª)
          example: 2

paths:
  /events/polling:
    get:
      summary: Buscar eventos pendentes
      description: |
        Retorna até 20 eventos não confirmados para o consumidor autenticado.
        Eventos são específicos por empresa (token) e por consumidor.
        Chamar respeitando o intervalo mínimo configurado no token (padrão 30s).
      tags: [Eventos]
      responses:
        '200':
          description: Lista de eventos pendentes (pode ser vazia)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Evento'
              example:
                - id: 42
                  code: PLC
                  nr_orcamen: 88
                  created_at: "2026-06-18 14:32:00"
                - id: 43
                  code: CFM
                  nr_orcamen: 88
                  created_at: "2026-06-18 14:35:10"
        '401':
          description: Token inválido ou não informado
        '429':
          description: Rate limit excedido
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErroRateLimit'

  /events/acknowledgment:
    post:
      summary: Confirmar eventos processados
      description: |
        Marca eventos como confirmados. Eventos confirmados não aparecem mais no próximo polling.
        Confirmar somente após processar com sucesso no sistema local.
      tags: [Eventos]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids:
                  type: array
                  items:
                    type: integer
                  example: [42, 43]
      responses:
        '200':
          description: Quantidade de eventos confirmados
          content:
            application/json:
              schema:
                type: object
                properties:
                  acknowledged:
                    type: integer
                    example: 2
        '401':
          description: Token inválido
        '422':
          description: Campo ids ausente ou inválido
        '429':
          description: Rate limit excedido

  /orders/{id}:
    get:
      summary: Detalhes do pedido
      description: |
        Retorna o pedido completo com itens, personalizações, endereço e pagamento.
        O campo `cd_pdvext` em cada item contém o código do produto no sistema externo,
        configurado pelo restaurante no cadastro do produto (campo "Código PDV").
      tags: [Pedidos]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Número do pedido (nr_orcamen)
      responses:
        '200':
          description: Pedido completo
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pedido'
        '401':
          description: Token inválido
        '404':
          description: Pedido não encontrado
        '429':
          description: Rate limit excedido

  /merchant/status:
    get:
      summary: Status de abertura do restaurante
      description: Retorna se o restaurante está aceitando pedidos de delivery e/ou balcão.
      tags: [Restaurante]
      responses:
        '200':
          description: Status atual
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MerchantStatus'
              example:
                ds_fantasi: "Pizzaria Boa Pedida"
                fl_entrega: true
                fl_balcoes: false

  /merchant/opening-hours:
    get:
      summary: Horários de funcionamento semanais
      description: |
        Retorna os horários configurados para cada dia da semana.
        Um dia pode ter múltiplas janelas (ex: almoço e jantar).
        Dias sem janelas = restaurante fechado naquele dia.
      tags: [Restaurante]
      responses:
        '200':
          description: Horários por dia da semana
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/HorarioDia'
              example:
                - dia_semana: 0
                  ds_dia: domingo
                  aberto: false
                  janelas: []
                - dia_semana: 1
                  ds_dia: segunda
                  aberto: true
                  janelas:
                    - abertura: "11:00"
                      fechamento: "14:00"
                    - abertura: "18:00"
                      fechamento: "22:00"

  /merchant/open:
    post:
      summary: Forçar abertura do restaurante
      description: Abre o restaurante imediatamente para delivery, balcão ou ambos.
      tags: [Restaurante]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                tipo:
                  type: string
                  enum: [entrega, balcao, ambos]
                  default: ambos
      responses:
        '200':
          description: Loja aberta
          content:
            application/json:
              example:
                message: "Loja aberta com sucesso."
                tipo: ambos

  /merchant/close:
    post:
      summary: Forçar fechamento do restaurante
      description: Fecha o restaurante imediatamente para delivery, balcão ou ambos.
      tags: [Restaurante]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                tipo:
                  type: string
                  enum: [entrega, balcao, ambos]
                  default: ambos
      responses:
        '200':
          description: Loja fechada
          content:
            application/json:
              example:
                message: "Loja fechada com sucesso."
                tipo: ambos

  /catalog/sync:
    post:
      summary: Enviar o catálogo completo
      description: |
        Cria e atualiza grupos e produtos casando pelo código do sistema externo.
        Roda em uma transação: falha no meio não deixa o cardápio pela metade.

        **Grupos de pizza:** o campo `tipo_pizza` liga o modo pizza, mas nunca o
        desliga — desligar tiraria bordas e massas do app sem aviso. Tamanhos,
        bordas e massas são configurados no painel do Nomli.

        **Preços:** `precos` é sempre um array. Em grupo comum, cada item vira um
        tamanho na ordem recebida. Em grupo de pizza, o casamento é pelo rótulo
        (`tamanho`) contra os tamanhos configurados no painel, e o preço vale para
        todas as combinações de quantidade de sabores daquele rótulo.

        O rótulo só é exigido quando há ambiguidade: produto com um único preço em
        grupo de pizza com um único tamanho dispensa `tamanho`.

        **Variações:** um item de `precos` com `codigo_externo` diferente do
        produto indica que aquele código é um tamanho, não um produto. Se ele já
        existir como produto, é desativado.

        **Descrição:** `observacao` só sobrescreve quando vem preenchida. Vazia,
        preserva o que o lojista escreveu no painel.
      tags: [Catálogo]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [grupos, produtos]
              properties:
                sync_id:
                  type: string
                  description: Identificador livre, usado só para rastreio no log.
                  example: "2026-08-06T09:10:00-0300"
                origem:
                  type: object
                  description: Metadados do sistema de origem. Opcional.
                grupos:
                  type: array
                  items:
                    type: object
                    required: [codigo_externo, nome]
                    properties:
                      codigo_externo: { type: string, maxLength: 50, example: "11" }
                      nome:           { type: string, maxLength: 30, example: "PIZZA SALGADA" }
                      ordem:          { type: integer, example: 3 }
                      ativo:          { type: boolean, default: true }
                      tipo_pizza:     { type: boolean, default: false }
                produtos:
                  type: array
                  items:
                    type: object
                    required: [codigo_externo, grupo_codigo_externo, nome, precos]
                    properties:
                      codigo_externo:       { type: string, maxLength: 50, example: "2" }
                      grupo_codigo_externo: { type: string, maxLength: 50, example: "11" }
                      nome:                 { type: string, maxLength: 100, example: "2 QUEIJOS" }
                      observacao:           { type: string, example: "Mussarela e catupiry, massa fina." }
                      ordem:                { type: integer, example: 1 }
                      ativo:                { type: boolean, default: true }
                      imagem_hash:
                        type: string
                        minLength: 32
                        maxLength: 32
                        description: MD5 do arquivo. Ausente = produto sem imagem no ERP (não remove a existente).
                        example: "9f2a1c7d4e8b3f60a1c2d3e4f5069a1b"
                      precos:
                        type: array
                        minItems: 1
                        items:
                          type: object
                          required: [preco]
                          properties:
                            codigo_externo: { type: string, maxLength: 50, example: "3" }
                            tamanho:
                              type: string
                              maxLength: 100
                              example: "BROTO"
                              description: |
                                Opcional quando não há ambiguidade. Em grupo comum, um preço sem
                                rótulo vira `UNICO`. Em grupo de pizza, o rótulo é dispensável se
                                o produto tiver um único preço e o grupo um único tamanho; havendo
                                mais de um dos dois lados, o preço é ignorado com aviso.
                            preco:          { type: number, format: float, example: 25.00 }
            example:
              sync_id: "2026-08-06T09:10:00-0300"
              grupos:
                - codigo_externo: "11"
                  nome: "PIZZA SALGADA"
                  ordem: 3
                  ativo: true
                  tipo_pizza: true
              produtos:
                - codigo_externo: "2"
                  grupo_codigo_externo: "11"
                  nome: "2 QUEIJOS"
                  ativo: true
                  ordem: 1
                  imagem_hash: "9f2a1c7d4e8b3f60a1c2d3e4f5069a1b"
                  precos:
                    - { codigo_externo: "2", tamanho: "GRANDE", preco: 38.00 }
                    - { codigo_externo: "3", tamanho: "BROTO",  preco: 25.00 }
      responses:
        '200':
          description: Catálogo processado
          content:
            application/json:
              example:
                sync_id: "2026-08-06T09:10:00-0300"
                grupos:   { criados: 2, atualizados: 11, desativados: 0 }
                produtos: { criados: 10, atualizados: 31, desativados: 3 }
                imagens_pendentes: ["2", "5", "16"]
                avisos:
                  - "grupo 11: criado sem categoria; defina no painel para a loja aparecer nos filtros do app"
                  - "produto 14: grupo 99 não encontrado, ignorado"
        '422':
          description: Payload inválido
          content:
            application/json:
              example:
                message: "The produtos.0.precos field is required."

  /catalog/image:
    post:
      summary: Enviar a imagem de um produto
      description: |
        Envie apenas as imagens que vieram em `imagens_pendentes` na resposta do
        `/catalog/sync`. A imagem entra na fila de aprovação do Nomli, igual ao
        upload feito pelo painel — não há atalho de moderação.

        O produto precisa existir: envie o catálogo antes das imagens.
      tags: [Catálogo]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [codigo_externo, imagem]
              properties:
                codigo_externo:
                  type: string
                  maxLength: 50
                  example: "2"
                imagem:
                  type: string
                  format: binary
                  description: JPG ou PNG, até 2 MB.
      responses:
        '200':
          description: Imagem recebida
          content:
            application/json:
              example:
                codigo_externo: "2"
                hash: "9f2a1c7d4e8b3f60a1c2d3e4f5069a1b"
                status: "aguardando_aprovacao"
        '422':
          description: Produto não encontrado ou arquivo inválido
          content:
            application/json:
              example:
                message: "Produto 2 não encontrado. Envie o catálogo antes das imagens."

tags:
  - name: Eventos
    description: Polling de eventos e confirmação (acknowledgment)
  - name: Pedidos
    description: Detalhes completos do pedido
  - name: Restaurante
    description: Status e horários de funcionamento
  - name: Catálogo
    description: Envio de grupos, produtos, preços e imagens a partir de um sistema externo
