openapi: 3.1.0
info:
  title: Sanctuware API
  version: 1.0.0
  description: API multi-organização. As chaves sw_live_* só acessam a igreja que as criou e toda escrita é auditada.
servers:
  - url: https://sanctuware.com/api/v1
security:
  - bearerAuth: []
tags:
  - {name: Recursos, description: CRUD dos módulos da igreja}
  - {name: Relatórios}
  - {name: IA}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sw_live_*
  parameters:
    Resource:
      name: resource
      in: path
      required: true
      schema:
        type: string
        enum: [members, congregations, events, attendance, finances, prayers, communications, studies, whatsapp-contacts, whatsapp-campaigns]
    Id:
      name: id
      in: path
      required: true
      schema: {type: string, format: uuid}
    Cursor:
      name: cursor
      in: query
      schema: {type: string}
    PerPage:
      name: per_page
      in: query
      schema: {type: integer, minimum: 1, maximum: 100, default: 25}
    UpdatedAfter:
      name: updated_after
      in: query
      description: Retorna somente registros atualizados após este instante.
      schema: {type: string, format: date-time}
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Chave única por organização e operação de criação.
      schema: {type: string, minLength: 8, maxLength: 255}
  headers:
    RateLimit:
      description: Limite de requisições por minuto do plano.
      schema: {type: integer}
    RateLimitRemaining:
      description: Requisições restantes na janela atual.
      schema: {type: integer}
  schemas:
    Resource:
      type: object
      additionalProperties: true
      required: [id]
      properties:
        id: {type: string, format: uuid}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    PaginationMeta:
      type: object
      required: [per_page]
      properties:
        per_page: {type: integer}
        next_cursor: {type: [string, 'null']}
    Links:
      type: object
      properties:
        first: {type: [string, 'null'], format: uri}
        last: {type: [string, 'null'], format: uri}
        prev: {type: [string, 'null'], format: uri}
        next: {type: [string, 'null'], format: uri}
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: {type: string, examples: [validation_error]}
            message: {type: string}
            details: {type: object, additionalProperties: true}
    InsightRequest:
      type: object
      required: [scope]
      properties:
        scope: {type: string, enum: [dashboard, members, member, finances, events, congregations, prayers, behavior]}
        subject_id: {type: [string, 'null'], format: uuid}
  responses:
    Unauthorized: {description: Chave ausente, inválida ou revogada, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
    Forbidden: {description: Escopo ou recurso não autorizado pelo plano, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
    RateLimited: {description: Limite por minuto excedido, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
paths:
  /{resource}:
    parameters:
      - {$ref: '#/components/parameters/Resource'}
    get:
      tags: [Recursos]
      summary: Lista recursos da organização
      description: Aceita filtros próprios do recurso; nunca retorna dados de outra organização.
      parameters:
        - {$ref: '#/components/parameters/Cursor'}
        - {$ref: '#/components/parameters/PerPage'}
        - {$ref: '#/components/parameters/UpdatedAfter'}
        - name: filter
          in: query
          style: deepObject
          explode: true
          description: Filtros exatos nos campos permitidos do recurso, por exemplo filter[status]=active.
          schema: {type: object, additionalProperties: true}
      responses:
        '200':
          description: Página de resultados
          headers:
            X-RateLimit-Limit: {$ref: '#/components/headers/RateLimit'}
            X-RateLimit-Remaining: {$ref: '#/components/headers/RateLimitRemaining'}
          content:
            application/json:
              schema:
                type: object
                required: [data, meta, links]
                properties:
                  data: {type: array, items: {$ref: '#/components/schemas/Resource'}}
                  meta: {$ref: '#/components/schemas/PaginationMeta'}
                  links: {$ref: '#/components/schemas/Links'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/RateLimited'}
    post:
      tags: [Recursos]
      summary: Cria um recurso
      parameters:
        - {$ref: '#/components/parameters/IdempotencyKey'}
      requestBody:
        required: true
        content: {application/json: {schema: {type: object, additionalProperties: true}}}
      responses:
        '201': {description: Criado, content: {application/json: {schema: {type: object, properties: {data: {$ref: '#/components/schemas/Resource'}}}}}}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '422': {description: Dados inválidos, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
        '429': {$ref: '#/components/responses/RateLimited'}
  /{resource}/{id}:
    parameters:
      - {$ref: '#/components/parameters/Resource'}
      - {$ref: '#/components/parameters/Id'}
    get:
      tags: [Recursos]
      summary: Consulta um recurso
      responses:
        '200': {description: Encontrado, content: {application/json: {schema: {type: object, properties: {data: {$ref: '#/components/schemas/Resource'}}}}}}
        '404': {description: Não encontrado nesta organização, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
    patch:
      tags: [Recursos]
      summary: Atualiza um recurso
      requestBody: {required: true, content: {application/json: {schema: {type: object, additionalProperties: true}}}}
      responses:
        '200': {description: Atualizado, content: {application/json: {schema: {type: object, properties: {data: {$ref: '#/components/schemas/Resource'}}}}}}
        '422': {description: Dados inválidos, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
    delete:
      tags: [Recursos]
      summary: Exclui um recurso
      responses:
        '204': {description: Excluído}
        '409': {description: Recurso possui dependências, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
  /reports:
    get:
      tags: [Relatórios]
      summary: Indicadores consolidados da organização
      responses:
        '200': {description: Indicadores em data/meta/links}
        '403': {$ref: '#/components/responses/Forbidden'}
  /ai/insights:
    post:
      tags: [IA]
      summary: Gera insight usando a carteira de IA da organização
      parameters:
        - {$ref: '#/components/parameters/IdempotencyKey'}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/InsightRequest'}}}
      responses:
        '201': {description: Insight criado e consumo contabilizado}
        '402': {description: Créditos insuficientes, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
        '403': {$ref: '#/components/responses/Forbidden'}
        '422': {description: Resposta inválida; reserva interna devolvida, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
  /files/studies/{organization}/{item}:
    get:
      security: []
      tags: [Recursos]
      summary: Baixa temporariamente um PDF privado de estudo
      description: URL assinada com validade de dez minutos, emitida no campo file_url do recurso studies.
      parameters:
        - {name: organization, in: path, required: true, schema: {type: string, format: uuid}}
        - {name: item, in: path, required: true, schema: {type: string, format: uuid}}
        - {name: expires, in: query, required: true, schema: {type: integer}}
        - {name: signature, in: query, required: true, schema: {type: string}}
      responses:
        '200': {description: Arquivo PDF privado}
        '403': {description: Assinatura inválida ou expirada}
        '404': {description: Arquivo não encontrado}
