openapi: 3.0.3

info:
  title: Espécimes de Assinaturas — Notários Portugueses
  version: "1.0.0"
  description: |
    API pública para consulta autorizada de espécimes de assinaturas de
    **notários**, **advogados** e **solicitadores** portugueses. Ao serviço
    da verificação de apostilas nos termos da Convenção de Haia.

    ## Autenticação

    Fluxo **OAuth 2.0 Client Credentials** com scope `signatures:read`.
    Cada consumidor recebe um par `client_id` / `client_secret` gerido no
    painel de administração. O token retornado é um `Bearer` que deve
    acompanhar cada pedido no header `Authorization`.

    ## URLs de imagem

    Cada espécime é devolvido com uma URL **pré-assinada** de curta duração
    (por defeito 300 s). Não guardes essa URL — pede um novo espécime
    sempre que precisares de a mostrar.

    ## Rate limiting e auditoria

    Todos os pedidos são registados na tabela `api_access_logs`. O uso é
    monitorizado para conformidade RGPD. Contactar `dpo@notarios.pt` para
    qualquer questão sobre acesso a dados pessoais.
  contact:
    name: Notários Portugueses
    url: https://notarios.pt
    email: dpo@notarios.pt
  license:
    name: Utilização restrita — Convenção de Haia (Apostila)
  termsOfService: https://especimes.notarios.pt/privacidade

servers:
  - url: https://especimes.notarios.pt
    description: Produção
  - url: https://dev.notarios.pt
    description: Desenvolvimento

tags:
  - name: Autenticação
    description: Emissão de tokens OAuth 2.0
  - name: Signatures
    description: Pesquisa e obtenção de espécimes de assinatura
  - name: Healthcheck
    description: Diagnóstico do serviço

security:
  - oauth2ClientCredentials: [signatures:read]

paths:
  /oauth/token:
    post:
      operationId: issueAccessToken
      tags: [Autenticação]
      summary: Emitir access token (client_credentials)
      description: |
        Troca `client_id` + `client_secret` por um `Bearer` token válido
        para as chamadas à API. O grant `client_credentials` é o único
        suportado publicamente — não há utilizadores finais.
      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
                  enum: [client_credentials]
                client_id:
                  type: string
                client_secret:
                  type: string
                scope:
                  type: string
                  default: "signatures:read"
      responses:
        '200':
          description: Token emitido
          content:
            application/json:
              schema:
                type: object
                required: [token_type, expires_in, access_token]
                properties:
                  token_type:
                    type: string
                    example: Bearer
                  expires_in:
                    type: integer
                    description: Validade em segundos
                    example: 31536000
                  access_token:
                    type: string
                    description: JWT / opaque bearer token
        '400':
          description: Grant ou parâmetros inválidos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthError'
        '401':
          description: Credenciais do cliente inválidas
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthError'

  /api/v1/ping:
    get:
      operationId: ping
      tags: [Healthcheck]
      summary: Ping simples (sem auth)
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  pong:
                    type: boolean
                    example: true
                  ts:
                    type: string
                    format: date-time

  /api/v1/signatures/search:
    get:
      operationId: searchSignatures
      tags: [Signatures]
      summary: Pesquisar profissionais e respetivos espécimes ativos
      description: |
        Devolve uma página de profissionais que têm pelo menos um espécime
        **ativo**. Todos os filtros são opcionais e combinam-se com `AND`.
        Cada espécime inclui uma URL pré-assinada de curta duração para a
        imagem PNG normalizada.
      parameters:
        - name: name
          in: query
          description: Pesquisa parcial (case-insensitive) sobre o nome
          schema: { type: string, maxLength: 120 }
          example: Pina
        - name: entity_type
          in: query
          schema:
            $ref: '#/components/schemas/EntityType'
        - name: professional_id
          in: query
          description: Cédula profissional / NIF (correspondência exata)
          schema: { type: string, maxLength: 64 }
          example: "16001"
        - name: country
          in: query
          description: Código ISO 3166-1 alpha-2
          schema: { type: string, minLength: 2, maxLength: 2, example: PT }
        - name: district
          in: query
          schema: { type: string, maxLength: 80 }
          example: Lisboa
        - name: county
          in: query
          schema: { type: string, maxLength: 80 }
        - name: parish
          in: query
          schema: { type: string, maxLength: 80 }
        - name: per_page
          in: query
          description: Itens por página (1–50)
          schema: { type: integer, minimum: 1, maximum: 50, default: 20 }
        - name: page
          in: query
          description: Número da página (paginação Laravel)
          schema: { type: integer, minimum: 1, default: 1 }
        - name: year
          in: query
          description: |
            Filtra pelo ano em que a pessoa teve **algum cargo ativo**
            (overlap com o ano civil). Ex.: `year=2020` devolve quem
            era notário/advogado/solicitador em algum momento de 2020.
            Mutuamente exclusivo com `as_of` (422 se ambos).
          schema: { type: integer, minimum: 1900, maximum: 2100, example: 2020 }
        - name: as_of
          in: query
          description: |
            Filtra pelos profissionais que tinham cargo ativo nessa
            data exata. Ex.: `as_of=2020-06-15`.
            Mutuamente exclusivo com `year`.
          schema: { type: string, format: date, example: "2020-06-15" }
      responses:
        '200':
          description: Página de resultados
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignatureSearchResult'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'

  /api/v1/signatures/{id}:
    get:
      operationId: getSignature
      tags: [Signatures]
      summary: Obter um espécime pelo ID
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
      responses:
        '200':
          description: Espécime encontrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Signature'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Espécime não encontrado ou inativo
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericError'

components:
  securitySchemes:
    oauth2ClientCredentials:
      type: oauth2
      description: |
        OAuth 2.0 Client Credentials (RFC 6749 §4.4). O token é opaco no
        cliente; o servidor mantém introspection em `/oauth/tokens`.
      flows:
        clientCredentials:
          tokenUrl: /oauth/token
          scopes:
            signatures:read: Ler pesquisa e detalhe de espécimes ativos

  schemas:
    EntityType:
      type: string
      description: |
        Categoria profissional. Novos valores podem ser introduzidos no
        futuro — os consumidores devem tolerar valores desconhecidos.
      enum: [notario, advogado, solicitador]

    User:
      type: object
      properties:
        id: { type: integer, example: 3 }
        name: { type: string, example: "João Tiago Silveira Pina" }
        entity_type: { $ref: '#/components/schemas/EntityType' }
        professional_id:
          type: string
          nullable: true
          description: |
            Cédula profissional em forma canónica. Para notários é o número
            sem prefixo "ONT" e sem zeros à esquerda (ex.: `16001`).
          example: "16001"
        country: { type: string, minLength: 2, maxLength: 2, example: PT }
        district: { type: string, nullable: true, example: Lisboa }
        county:   { type: string, nullable: true }
        parish:   { type: string, nullable: true }

    Position:
      description: |
        Um cargo que a pessoa deteve/detem. Múltiplas posições podem estar
        ativas simultaneamente (acumulação). `ended_at=null` marca cargo atual.
      type: object
      properties:
        id: { type: integer, example: 12 }
        entity_type: { $ref: '#/components/schemas/EntityType' }
        professional_id: { type: string, nullable: true, example: "16001" }
        office_name: { type: string, nullable: true, example: "Cartório Notarial de Lisboa" }
        country: { type: string, minLength: 2, maxLength: 2, example: PT }
        district: { type: string, nullable: true, example: Lisboa }
        county: { type: string, nullable: true }
        parish: { type: string, nullable: true }
        started_at: { type: string, format: date, example: "2010-03-01" }
        ended_at: { type: string, format: date, nullable: true, example: null }
        is_primary: { type: boolean }

    Specimen:
      type: object
      properties:
        id: { type: integer, example: 1 }
        position_id:
          type: integer
          nullable: true
          description: Cargo em vigor no momento em que a assinatura foi recolhida
        url:
          type: string
          format: uri
          description: URL pré-assinada (S3/MinIO) para a imagem PNG
          example: "https://especimes-cdn.notarios.pt/signatures/users/3/01HXY...png?X-Amz-Signature=..."
        expires_at:
          type: string
          format: date-time
          description: Momento em que a URL pré-assinada expira
        sha256_hash:
          type: string
          minLength: 64
          maxLength: 64
          description: SHA-256 do PNG normalizado (usado para de-duplicação)
        width_px: { type: integer, example: 800 }
        height_px: { type: integer, example: 260 }
        captured_at:
          type: string
          format: date-time
          nullable: true

    UserWithSpecimens:
      allOf:
        - $ref: '#/components/schemas/User'
        - type: object
          properties:
            positions:
              type: array
              description: Histórico completo de cargos (ordenado do mais recente para o mais antigo)
              items: { $ref: '#/components/schemas/Position' }
            specimens:
              type: array
              items: { $ref: '#/components/schemas/Specimen' }

    Signature:
      type: object
      properties:
        id: { type: integer, example: 1 }
        user: { $ref: '#/components/schemas/User' }
        position:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/Position'
        url:  { type: string, format: uri }
        expires_at: { type: string, format: date-time }
        sha256_hash: { type: string, minLength: 64, maxLength: 64 }
        width_px: { type: integer }
        height_px: { type: integer }
        captured_at: { type: string, format: date-time, nullable: true }

    SignatureSearchResult:
      description: Paginação padrão do Laravel
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/UserWithSpecimens' }
        current_page: { type: integer, example: 1 }
        from: { type: integer, nullable: true, example: 1 }
        last_page: { type: integer, example: 3 }
        per_page: { type: integer, example: 20 }
        to: { type: integer, nullable: true, example: 20 }
        total: { type: integer, example: 45 }
        first_page_url: { type: string, format: uri }
        last_page_url:  { type: string, format: uri }
        next_page_url:  { type: string, format: uri, nullable: true }
        prev_page_url:  { type: string, format: uri, nullable: true }
        path: { type: string, format: uri }
        links:
          type: array
          items:
            type: object
            properties:
              url:    { type: string, format: uri, nullable: true }
              label:  { type: string }
              active: { type: boolean }

    OAuthError:
      type: object
      properties:
        error: { type: string, example: invalid_client }
        error_description: { type: string }
        hint: { type: string }
        message: { type: string }

    ValidationError:
      type: object
      properties:
        message: { type: string, example: "The given data was invalid." }
        errors:
          type: object
          additionalProperties:
            type: array
            items: { type: string }

    GenericError:
      type: object
      properties:
        message: { type: string }

  responses:
    Unauthenticated:
      description: "Falta o `Authorization: Bearer …` ou o token é inválido"
      content:
        application/json:
          schema: { $ref: '#/components/schemas/GenericError' }
    Forbidden:
      description: Token válido mas sem o scope `signatures:read`
      content:
        application/json:
          schema: { $ref: '#/components/schemas/GenericError' }
    ValidationError:
      description: Parâmetros inválidos
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ValidationError' }
