# O contrato do webhook de coberturas do Domus.
#
# ## Por que OpenAPI, e não só a página escrita
#
# A página explica; este arquivo DEFINE. Quem integra gera tipos e cliente a partir dele, em
# vez de traduzir prosa — e o CI valida um payload real do nosso código contra estes schemas,
# então a documentação não consegue mentir em silêncio.
#
# Isso não é hipotético: em 09/09/2026 a documentação prometia um evento `coverage.restored`
# que o código nunca disparava. Ninguém percebeu por um dia.
#
# ## `webhooks:`, e não `paths:`
#
# Esta seção existe no OpenAPI 3.1 exatamente para isto: descrever requisições que NÓS
# enviamos, para um endereço que o parceiro fornece. Não há `paths` aqui porque não há
# endpoint nosso a chamar — a integração é de via única, do Domus para fora.
#
# ## Nada de dado real
#
# Todo exemplo usa nome, telefone e endereço inventados. É documentação pública.
openapi: 3.1.0

info:
  title: Webhook de coberturas — Domus
  version: '1.0.0'
  summary: Avisa um sistema parceiro quando uma cobertura muda.
  description: |
    Sempre que uma rede, área, setor ou Life Group é criado, alterado, arquivado ou
    desarquivado no Domus, um `POST` assinado sai para o endereço cadastrado, em segundos.

    **Via única.** O corpo da resposta é descartado; o Domus só olha o código de status.

    Cada evento carrega o **estado atual completo** daquela cobertura, nunca um diff — se um
    evento se perder ou chegar fora de ordem, o próximo conserta sozinho.
  contact:
    name: Equipe Domus
    url: https://doc.domusapp.plus

servers:
  - url: https://o-endereco-que-voces-informarem.exemplo
    description: |
      O destino é fornecido por vocês e cadastrado no Domus. Só HTTPS é aceito.

webhooks:
  coverageEvent:
    post:
      summary: Um evento de cobertura
      operationId: receberEventoDeCobertura
      description: |
        O Domus envia isto para o endereço cadastrado. Responda **2xx** para confirmar.

        Qualquer outra resposta é tratada como falha e a entrega volta com recuo:
        30s, 2min, 10min, 30min, 2h, 4h. Esgotadas as seis, uma varredura diária refaz —
        nada se perde, só atrasa.
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/Timestamp'
        - $ref: '#/components/parameters/Signature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Evento'
            examples:
              lifeAlterado:
                summary: Um Life Group teve o dia de reunião alterado
                value:
                  event: coverage.updated
                  event_id: 9f1c8a20-4d7e-4b31-9a6c-2f0e5b1d8c33
                  occurred_at: '2026-09-10T20:31:02.418Z'
                  cobertura:
                    coverage_id: 128
                    nivel: life
                    nivel_nome: Life Group
                    nome: Life Aurora
                    nome_life: Life Aurora
                    parent_id: 77
                    path_ids: [441, 1, 5, 24, 77, 128]
                    dia: QUARTA
                    hora: '20:00'
                    tipo: Adultos
                    tipo_key: adult
                    modalidade: Presencial
                    lider: Letícia Prado
                    tel_lider: '+5511900000001'
                    lideres:
                      - user_id: 812
                        nome: Letícia Prado
                        telefone: '+5511900000001'
                    setor_id: 77
                    supervisor_setor: Caio e Camila Ferrez
                    tel_supervisor_setor: '+5511900000002'
                    area_id: 24
                    supervisor_area: Cláudia Menezes
                    tel_supervisor_area: '+5511900000003'
                    rede_id: 5
                    pastor_rede: Rubens e Débora Campos
                    tel_pastor_rede: '+5511900000004'
                    igreja_id: 1
                    igreja: Igreja Central
                    campus_id: 3
                    campus: Sede
                    endereco: Rua de Exemplo
                    numero: '480'
                    complemento: ''
                    cep: 00000-000
                    bairro: Bairro Modelo
                    cidade: São Paulo
                    estado: SP
                    latitude: -23.4842
                    longitude: -46.6205
                    is_archived: false
                    updated_at: '2026-09-10T20:31:00.912Z'
              redeRenomeada:
                summary: Uma rede foi renomeada — um evento só, não um por life abaixo dela
                value:
                  event: coverage.updated
                  event_id: 3ab77c14-90e2-4f55-8d10-7c6b2a4e9011
                  occurred_at: '2026-09-10T20:44:10.006Z'
                  cobertura:
                    coverage_id: 5
                    nivel: rede
                    nivel_nome: Rede
                    nome: Rede Norte
                    parent_id: 1
                    path_ids: [441, 1, 5]
                    lideres:
                      - user_id: 589
                        nome: Rubens Campos
                        telefone: '+5511900000004'
                    is_archived: false
                    updated_at: '2026-09-10T20:44:08.220Z'
              teste:
                summary: Disparado por um humano na tela de configuração
                value:
                  event: coverage.ping
                  event_id: 340da467-b97f-47e1-994e-f498c79bab85
                  occurred_at: '2026-09-10T17:58:15.000Z'
                  mensagem: Teste de configuração do Domus. Nada mudou.
      responses:
        '200':
          description: |
            Aceito. Qualquer 2xx serve — `204` também.

            **Responda 2xx para o evento repetido também.** Retentativa entrega duas vezes;
            devolver erro num evento já aplicado faria a fila insistir para sempre.
        '401':
          description: |
            Assinatura inválida ou timestamp fora da janela. É a resposta CERTA nesses casos —
            aceitar em silêncio o que não valida esconderia um segredo trocado.
        '5xx':
          description: Falha temporária. O Domus tenta de novo com recuo.

components:
  parameters:
    EventId:
      name: X-Domus-Event-Id
      in: header
      required: true
      description: |
        UUID **estável entre as tentativas** da mesma entrega. Guarde-o e descarte o repetido:
        é a única forma de não aplicar duas vezes o mesmo evento.
      schema:
        type: string
        format: uuid
      example: 9f1c8a20-4d7e-4b31-9a6c-2f0e5b1d8c33

    Timestamp:
      name: X-Domus-Timestamp
      in: header
      required: true
      description: |
        Unix em segundos, no momento do envio. Entra na assinatura — recuse o que tiver mais de
        **5 minutos**, ou a chamada capturada hoje continua válida daqui a um mês.
      schema:
        type: string
        pattern: '^[0-9]+$'
      example: '1757534262'

    Signature:
      name: X-Domus-Signature
      in: header
      required: true
      description: |
        `sha256=<hex>`, o HMAC-SHA256 de `timestamp + "." + corpo` com o segredo combinado.

        O corpo é o texto **cru** recebido, byte a byte. Reserializar o JSON antes de conferir
        é o erro mais comum de quem implementa isto.
      schema:
        type: string
        pattern: '^sha256=[0-9a-f]{64}$'
      example: sha256=6b1f0c9d2e4a8b7c5d3e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b09

  schemas:
    Evento:
      type: object
      required: [event, event_id, occurred_at]
      properties:
        event:
          $ref: '#/components/schemas/TipoDeEvento'
        event_id:
          type: string
          format: uuid
          description: Igual ao cabeçalho `X-Domus-Event-Id`.
        occurred_at:
          type: string
          format: date-time
          description: Quando a mudança foi registrada no Domus — não quando esta tentativa saiu.
        cobertura:
          $ref: '#/components/schemas/Cobertura'
        mensagem:
          type: string
          description: Só em `coverage.ping`.
      additionalProperties: false

    TipoDeEvento:
      type: string
      enum:
        - coverage.created
        - coverage.updated
        - coverage.archived
        - coverage.restored
        - coverage.ping
      description: |
        - `coverage.created` — a cobertura passou a existir
        - `coverage.updated` — algum campo do retrato mudou
        - `coverage.archived` — foi arquivada (`is_archived` vira `true`; nada é apagado)
        - `coverage.restored` — voltou a valer
        - `coverage.ping` — teste de configuração, disparado por um humano; não representa
          mudança nenhuma

    Nivel:
      type: string
      enum: [life, setor, area, rede, igreja, outro]
      description: |
        O nível na hierarquia. **Só `life` traz os campos de reunião e endereço** — dia, tipo,
        modalidade, campus e coordenadas não existem nos níveis acima.

    Lider:
      type: object
      required: [user_id, nome, telefone]
      properties:
        user_id:
          type: integer
          description: Estável. É por ele que se casa uma pessoa entre eventos.
        nome:
          type: string
        telefone:
          type: string
          description: E.164, sem formatação. Vazio quando a pessoa não tem telefone cadastrado.
      additionalProperties: false

    Cobertura:
      type: object
      description: |
        O retrato **completo e atual** da cobertura. Nunca um diff.

        Os campos marcados como "só em life" simplesmente não aparecem nos outros níveis — não
        vêm vazios.
      required: [coverage_id, nivel, nome, parent_id, path_ids, lideres, is_archived, updated_at]
      properties:
        coverage_id:
          type: integer
          description: A identidade. Case por ele, nunca por nome.
        nivel:
          $ref: '#/components/schemas/Nivel'
        nivel_nome:
          type: ['string', 'null']
          description: O nome da hierarquia como cadastrado. Rótulo; a autoridade é `nivel`.
        nome:
          type: string
        parent_id:
          type: ['integer', 'null']
          description: A cobertura imediatamente acima. `null` só na raiz.
        path_ids:
          type: array
          items: { type: integer }
          description: |
            A linhagem inteira, da raiz até esta cobertura, inclusive. Mudar a cobertura-pai
            reescreve esta lista — é por ela que se re-pendura a árvore sem um evento de
            "movido".
        lideres:
          type: array
          items: { $ref: '#/components/schemas/Lider' }
          description: Ordenada por `user_id`, de forma estável entre eventos.
        is_archived:
          type: boolean
        updated_at:
          type: string
          format: date-time

        nome_life:
          type: string
          description: Só em life. Igual a `nome`; existe para paridade com a planilha.
        dia:
          type: string
          description: Só em life. `DOMINGO` … `SABADO`, ou vazio quando não cadastrado.
        hora:
          type: string
          description: Só em life. `HH:MM`.
        tipo:
          type: string
          description: Só em life. O rótulo EXIBIDO — muda quando alguém corrige o cadastro.
        tipo_key:
          type: string
          description: |
            Só em life. A identidade estável do tipo: `adult`, `young`, `teen`, `kids`.
            **Case por esta, não por `tipo`.**
        modalidade:
          type: string
          description: Só em life.
        lider:
          type: string
          description: Só em life. Os nomes de `lideres`, concatenados. Conveniência.
        tel_lider:
          type: string
          description: Só em life. Os telefones de `lideres`, concatenados.

        setor_id: { type: ['integer', 'null'], description: Só em life. }
        supervisor_setor: { type: string, description: Só em life. Ver a nota sobre ancestrais. }
        tel_supervisor_setor: { type: string, description: Só em life. }
        area_id: { type: ['integer', 'null'], description: Só em life. }
        supervisor_area: { type: string, description: Só em life. }
        tel_supervisor_area: { type: string, description: Só em life. }
        rede_id: { type: ['integer', 'null'], description: Só em life. }
        pastor_rede: { type: string, description: Só em life. }
        tel_pastor_rede: { type: string, description: Só em life. }
        igreja_id: { type: ['integer', 'null'], description: Só em life. }
        igreja: { type: string, description: Só em life. }
        campus_id: { type: ['integer', 'null'], description: Só em life. }
        campus: { type: string, description: Só em life. }

        endereco: { type: string, description: Só em life. }
        numero: { type: string, description: Só em life. }
        complemento: { type: string, description: Só em life. }
        cep: { type: string, description: Só em life. }
        bairro: { type: string, description: Só em life. }
        cidade: { type: string, description: Só em life. }
        estado: { type: string, description: Só em life. }
        latitude: { type: ['number', 'null'], description: Só em life. }
        longitude: { type: ['number', 'null'], description: Só em life. }
      additionalProperties: false
