{
  "openapi": "3.0.3",
  "info": {
    "title": "FarMapp API",
    "version": "2.10.0",
    "description": "Erros: todas as respostas 4xx/5xx usam o schema `Erro` — { erro: true, mensagem: string }.\n\n## Rate limiting (RFC 9652)\n\nTodas as respostas de /api/* (exceto /api/health) incluem `RateLimit-Policy` e `RateLimit` (draft-7, janela 300s). Limites: 120 pedidos/5min anónimo, 300/5min autenticado. No limite: 429 com `Retry-After`. Endpoints de auth e trajectória têm limites próprios.\n\n## Versionamento e depreciação\n\nVersão atual da API: 1 — todas as respostas trazem `X-FarMapp-API-Version: 1`. O path /api é a v1 (sem prefixo). Mudanças incompatíveis evoluem em /api/v2+. Rotas deprecadas respondem com `Deprecation: true` e `Sunset: <data>` (RFC 8594); período típico de aviso 6 meses. `info.version` é a versão deste documento, não da API.\n\n## Erros\nTodas as respostas 4xx/5xx usam o schema `Erro`: { erro: true, code, mensagem } (detalhes opcional). `code` é o identificador máquina estável — agents devem reagir por `code`, não por `mensagem`. Exemplos: `NAO_AUTENTICADO`/`TOKEN_EM_FALTA` (retomar sessão), `LIMITE_DE_PEDIDOS` (429; esperar `Retry-After`), `NAO_ENCONTRADO`, `CONFLITO`, `PEDIDO_INVALIDO`, `ERRO_INTERNO`. A lista completa está no `enum` do schema `Erro`."
  },
  "servers": [
    {
      "url": "https://farmaperto.onrender.com",
      "description": "Produção (Render)"
    }
  ],
  "tags": [
    {
      "name": "Saúde",
      "description": "Estado do serviço"
    },
    {
      "name": "Autenticação",
      "description": "Registo e início de sessão"
    },
    {
      "name": "Farmácias",
      "description": "Consulta e gestão de farmácias"
    },
    {
      "name": "Avaliações",
      "description": "Avaliações de farmácias"
    },
    {
      "name": "Reservas",
      "description": "Reserva de medicamentos"
    },
    {
      "name": "Perfil",
      "description": "Perfil de farmácia (stock, fotos, acessos)"
    },
    {
      "name": "Denúncias",
      "description": "Denúncia de comportamentos abusivos"
    },
    {
      "name": "Administração",
      "description": "Gestão interna da plataforma"
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "tags": [
          "Sistema"
        ],
        "summary": "Health check",
        "responses": {
          "200": {
            "description": "API operacional.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "OK"
                      ]
                    },
                    "servico": {
                      "type": "string"
                    },
                    "dataHora": {
                      "type": "string",
                      "format": "date-time"
                    }
                  },
                  "required": [
                    "status",
                    "servico",
                    "dataHora"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "getHealth",
        "description": "Verificação de saúde do backend. Retorna o estado do serviço e a data/hora do servidor. Não requer autenticação. Útil para monitorização e para confirmar que a API está operacional."
      }
    },
    "/api/auth/register": {
      "post": {
        "tags": [
          "Autenticação"
        ],
        "summary": "Criar conta de utilizador",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "nome": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Nome completo do utilizador."
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Email único — usado para login."
                  },
                  "telefone": {
                    "type": "string",
                    "description": "Número de telefone (opcional, mas recomendado para reservas via SMS).",
                    "nullable": true
                  },
                  "password": {
                    "type": "string",
                    "minLength": 6,
                    "description": "Palavra-passe (mínimo 6 caracteres)."
                  }
                },
                "required": [
                  "nome",
                  "email",
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Conta criada com sucesso. O campo `user.role` é sempre `user` neste endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "Conflito de estado (recurso já reservado, inativo ou duplicado).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "registarUtilizador",
        "description": "Regista um novo utilizador no FarMapp. O email tem de ser único — se já estiver registado, retorna 409. A password é hasheada com bcrypt antes de ser guardada. Retorna um token JWT (validade 7 dias) e os dados do utilizador, permitindo fazer login imediatamente."
      }
    },
    "/api/auth/login": {
      "post": {
        "tags": [
          "Autenticação"
        ],
        "summary": "Iniciar sessão (utilizador ou admin)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string"
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sessão iniciada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "iniciarSessao",
        "description": "Autentica um utilizador existente pelo email e password. O email é comparado sem distinguir maiúsculas/minúsculas. Se a conta estiver desativada (ex. por um admin), retorna 403. O campo `gestor_admins` é incluído apenas quando `role` é `admin`."
      }
    },
    "/api/auth/farmacia": {
      "post": {
        "tags": [
          "Autenticação"
        ],
        "summary": "Iniciar sessão como farmácia (via ID de 10 dígitos)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "codigo": {
                    "type": "string",
                    "description": "ID de 10 dígitos atribuído à farmácia pelo admin. Apenas dígitos são aceites — caracteres não numéricos são ignorados."
                  },
                  "localidade": {
                    "type": "string",
                    "description": "Nome da localidade de onde está a aceder (opcional, registado para auditoria).",
                    "nullable": true
                  }
                },
                "required": [
                  "codigo"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sessão iniciada com sucesso para a farmácia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmaciaTokenResponse"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "iniciarSessaoFarmacia",
        "description": "Autentica uma farmácia usando o seu código de acesso de 10 dígitos (campo `codigo_acesso`). O pedido é registado em `farmacia_acessos` (IP, user-agent, localidade). O token retornou contém `role: 'farmacia'` e `farmacia_id`."
      }
    },
    "/api/auth/google": {
      "post": {
        "tags": [
          "Autenticação"
        ],
        "summary": "Iniciar sessão com Google (ID token)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "credential": {
                    "type": "string",
                    "description": "ID token emitido pelo Google Identity Services (GIS). Gerado no frontend com `google.accounts.id.initialize()`."
                  }
                },
                "required": [
                  "credential"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sessão iniciada com Google.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "iniciarSessaoGoogle",
        "description": "Autentica ou cria uma conta usando o ID token do Google. Se o email ainda não existir, cria automaticamente a conta com `provider: 'google'` e sem password. Se existir (conta de email), faz login directamente. Se o `GOOGLE_CLIENT_ID` não estiver configurado no servidor, retorna 503."
      }
    },
    "/api/auth/me": {
      "get": {
        "tags": [
          "Autenticação"
        ],
        "summary": "Obter dados do utilizador autenticado",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Dados do utilizador.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "user": {
                      "$ref": "#/components/schemas/UserInfo"
                    }
                  },
                  "required": [
                    "erro",
                    "user"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "obterPerfilUtilizador",
        "description": "Retorna os dados básicos do utilizador cujo token foi usado no pedido. Campos: `id`, `name`, `email`, `role`. Não retorna `telefone` — use `GET /api/perfil/me` para o perfil completo da farmácia."
      }
    },
    "/api/farmacias": {
      "get": {
        "tags": [
          "Farmácias (público)"
        ],
        "summary": "Listar e pesquisar farmácias (mapa/lista)",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "description": "Texto de pesquisa. Depende de `search_type`: nome/morada/bairro/município da farmácia (default e `farmacia`), item de stock (`medicamento`) ou localidade (`bairro`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search_type",
            "in": "query",
            "description": "Tipo de pesquisa: `farmacia` (nome/morada/localidade — default), `medicamento` (itens de stock com o termo) ou `bairro` (arbitro por bairro/município).",
            "schema": {
              "type": "string",
              "enum": [
                "farmacia",
                "medicamento",
                "bairro"
              ],
              "default": "farmacia"
            }
          },
          {
            "name": "categoria",
            "in": "query",
            "description": "Filtrar por categoria de item de stock (apenas relevante com `search_type=medicamento`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "subcategoria",
            "in": "query",
            "description": "Filtrar por subcategoria de item de stock (apenas relevante com `search_type=medicamento`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "latitude",
            "in": "query",
            "description": "Latitude do utilizador (decimal). Também aceite como `lat`. Ordena por proximidade quando fornecida com longitude.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "longitude",
            "in": "query",
            "description": "Longitude do utilizador (decimal). Também aceite como `lng`. Ordena por proximidade quando fornecida com latitude.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "raio_km",
            "in": "query",
            "description": "Filtrar por raio máximo em km (requer latitude+longitude).",
            "schema": {
              "type": "number",
              "format": "double",
              "minimum": 0
            }
          },
          {
            "name": "somente_ativas",
            "in": "query",
            "description": "`true` devolve apenas farmácias activas (pagamento regularizado). Utilizadores normais só vêem activas; admins/registadores podem pedir `false`.",
            "schema": {
              "type": "boolean",
              "default": "true"
            }
          },
          {
            "name": "limite",
            "in": "query",
            "description": "Número máximo de resultados (padrão: 50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "registadas_por_mim",
            "in": "query",
            "description": "Só para registadores autenticados: `true` devolve apenas farmácias que o próprio registou.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Array de farmácias (vazio se nenhuma corresponder aos filtros).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/FarmaciaResumo"
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Parâmetros da requisição inválidos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            }
          }
        },
        "operationId": "listarFarmacias",
        "description": "Endpoint público — não requer autenticação. Retorna um array de farmácias com dados de contacto, coordenadas GPS, média de avaliações e total. Filtros: `search` + `search_type` (farmacia/medicamento/bairro), `categoria`/`subcategoria` (com medicamento), `latitude`/`longitude` (ordenação por proximidade) e `raio_km` (filtro de raio). Quando `search_type=medicamento`, cada farmácia inclui o array `stock` com os itens correspondentes. Registadores autenticados podem usar `registadas_por_mim=true`."
      },
      "post": {
        "tags": [
          "Farmácias"
        ],
        "summary": "Criar farmácia (admin/gestor)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "201": {
            "description": "Farmácia criada com sucesso.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "mensagem": {
                      "type": "string"
                    },
                    "farmacia": {
                      "$ref": "#/components/schemas/FarmaciaCompleta"
                    }
                  },
                  "required": [
                    "erro",
                    "mensagem",
                    "farmacia"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Sem permissão para realizar esta operação.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "Conflito de estado (recurso já reservado, inativo ou duplicado).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "criarFarmacia",
        "description": "Regista uma nova farmácia. Gera automaticamente um código de acesso de 10 dígitos (`codigo_acesso`), envia SMS com o ID à farmácia e notifica todos os admins por email. Requer papel `admin` ou `gestor`. A longitude é guardada internamente em PostGIS (campo `geom`).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "nome": {
                    "type": "string",
                    "minLength": 3,
                    "description": "Nome da farmácia."
                  },
                  "telefone": {
                    "type": "string",
                    "description": "Telefone principal — obrigatório."
                  },
                  "endereco": {
                    "type": "string",
                    "description": "Morada / endereço físico.",
                    "nullable": true
                  },
                  "bairro": {
                    "type": "string",
                    "nullable": true
                  },
                  "municipio": {
                    "type": "string",
                    "nullable": true
                  },
                  "latitude": {
                    "type": "number",
                    "format": "double",
                    "description": "Latitude em graus decimais (ex.: -8.8390)."
                  },
                  "longitude": {
                    "type": "number",
                    "format": "double",
                    "description": "Longitude em graus decimais (ex.: 13.2894)."
                  },
                  "horario_abertura": {
                    "type": "string",
                    "description": "Hora de abertura (HH:MM, ex.: '08:00').",
                    "nullable": true
                  },
                  "horario_fecho": {
                    "type": "string",
                    "nullable": true
                  },
                  "is_24_horas": {
                    "type": "boolean",
                    "default": false
                  },
                  "servicos": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Ex.: ['Entrega','Reserva','Atendimento 24h']."
                  },
                  "notas": {
                    "type": "string",
                    "nullable": true
                  }
                },
                "required": [
                  "nome",
                  "telefone",
                  "latitude",
                  "longitude"
                ]
              }
            }
          }
        }
      }
    },
    "/api/farmacias/{id}": {
      "get": {
        "tags": [
          "Farmácias (público)"
        ],
        "summary": "Obter farmácia por UUID",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID do recurso (farmácia, reserva, item, etc.)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Dados completos da farmácia com stock.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmaciaCompleta"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "obterFarmacia",
        "description": "Retorna os dados completos de uma farmácia pelo seu UUID — inclui coordenadas, horários, serviços, média e total de avaliações, e o array `stock` com todos os itens disponíveis (quantidade > 0). Não requer autenticação. Retorna 404 se não encontrada."
      },
      "put": {
        "tags": [
          "Farmácias"
        ],
        "summary": "Atualizar dados da farmácia (admin)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Farmácia actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "mensagem": {
                      "type": "string"
                    },
                    "farmacia": {
                      "$ref": "#/components/schemas/FarmaciaCompleta"
                    }
                  },
                  "required": [
                    "erro",
                    "mensagem",
                    "farmacia"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Sem permissão para realizar esta operação.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "atualizarFarmacia",
        "description": "Atualiza qualquer campo da farmácia identificada pelo UUID. Requer papel `admin`. A actualização do campo `is_ativo` deve ser feita via `PATCH /api/farmacias/{id}/status`. Retorna os dados actualizados com `latitude` e `longitude` em formato float.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID do recurso (farmácia, reserva, item, etc.)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "nome": {
                    "type": "string",
                    "minLength": 3
                  },
                  "telefone": {
                    "type": "string"
                  },
                  "telefone_secundario": {
                    "type": "string",
                    "nullable": true
                  },
                  "endereco": {
                    "type": "string",
                    "nullable": true
                  },
                  "bairro": {
                    "type": "string",
                    "nullable": true
                  },
                  "municipio": {
                    "type": "string",
                    "nullable": true
                  },
                  "latitude": {
                    "type": "number",
                    "format": "double"
                  },
                  "longitude": {
                    "type": "number",
                    "format": "double"
                  },
                  "horario_abertura": {
                    "type": "string",
                    "nullable": true
                  },
                  "horario_fecho": {
                    "type": "string",
                    "nullable": true
                  },
                  "is_24_horas": {
                    "type": "boolean"
                  },
                  "servicos": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "is_ativo": {
                    "type": "boolean"
                  },
                  "notas": {
                    "type": "string",
                    "nullable": true
                  }
                },
                "required": [
                  "nome",
                  "telefone",
                  "latitude",
                  "longitude"
                ]
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Farmácias"
        ],
        "summary": "Eliminar farmácia (admin)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Farmácia eliminada.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "mensagem": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "erro",
                    "mensagem"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Sem permissão para realizar esta operação.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "eliminarFarmacia",
        "description": "Elimina permanentemente uma farmácia e todos os seus dados associados (stock, avaliações, reservas). Esta operação é irreversível. Requer papel `admin`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID do recurso (farmácia, reserva, item, etc.)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/api/farmacias/{id}/avaliacoes": {
      "get": {
        "tags": [
          "Avaliações"
        ],
        "summary": "Obter média e total de avaliações da farmácia",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID do recurso (farmácia, reserva, item, etc.)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Estatísticas de avaliação da farmácia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvaliacaoStats"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            }
          }
        },
        "operationId": "obterAvaliacoesFarmacia",
        "description": "Retorna a média arredondada a 1 casa decimal e o número total de avaliações da farmácia. Não requer autenticação."
      },
      "post": {
        "tags": [
          "Avaliações"
        ],
        "summary": "Avaliar ou actualizar avaliação de uma farmácia",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "201": {
            "description": "Avaliação criada ou actualizada com estatísticas atualizadas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvaliacaoCriada"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "avaliarFarmacia",
        "description": "Cria uma avaliação (rating 1–5) para a farmácia. Se o utilizador já tiver avaliado, actualiza a nota existente (upsert). Retorna a avaliação criada/actualizada e as estatísticas actualizadas. Requer autenticação — cada utilizador pode ter apenas uma avaliação por farmácia.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID do recurso (farmácia, reserva, item, etc.)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rating": {
                    "type": "integer",
                    "enum": [
                      1,
                      2,
                      3,
                      4,
                      5
                    ],
                    "description": "Nota de 1 a 5."
                  }
                },
                "required": [
                  "rating"
                ]
              }
            }
          }
        }
      }
    },
    "/api/reservas": {
      "post": {
        "tags": [
          "Reservas"
        ],
        "summary": "Criar reserva de medicamento (3 horas)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "stock_item_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "UUID do item em stock a reservar. O item tem de ter `quantidade > 0` e não pode estar já reservado (campo `reservado_ate` no futuro)."
                  },
                  "quantidade": {
                    "type": "integer",
                    "minimum": 1,
                    "default": 1,
                    "description": "Número de unidades a reservar. Não pode exceder o stock disponível."
                  }
                },
                "required": [
                  "stock_item_id"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Reserva criada com sucesso.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "reserva": {
                      "$ref": "#/components/schemas/Reserva"
                    }
                  },
                  "required": [
                    "erro",
                    "reserva"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Sem permissão para realizar esta operação.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "Conflito de estado (recurso já reservado, inativo ou duplicado).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "criarReserva",
        "description": "Reserva um item de stock de uma farmácia durante 3 horas. O stock é automaticamente decrementado e o campo `reservado_ate` é actualizado. A farmácia recebe um SMS de notificação com o nome do utilizador e hora limite. Requer autenticação — o utilizador não pode ser o dono da farmácia (implícito no frontend). Tempo de expiração: 3 horas a partir do momento da reserva."
      },
      "get": {
        "tags": [
          "Reservas"
        ],
        "summary": "Listar reservas da farmácia autenticada",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de reservas da farmácia.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "reservas": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReservaComNomes"
                      }
                    }
                  },
                  "required": [
                    "erro",
                    "reservas"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Sem permissão para realizar esta operação.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "listarReservasFarmacia",
        "description": "Retorna todas as reservas associadas à farmácia autenticada (token com `role: 'farmacia'`), da mais recente para a mais antiga. Inclui o nome do utilizador, o telefone (para contacto) e o nome do produto. Requer autenticação como farmácia."
      }
    },
    "/api/reservas/minhas": {
      "get": {
        "tags": [
          "Reservas"
        ],
        "summary": "Listar reservas do utilizador autenticado",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de reservas do utilizador.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "reservas": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReservaComNomes"
                      }
                    }
                  },
                  "required": [
                    "erro",
                    "reservas"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "listarMinhasReservas",
        "description": "Retorna todas as reservas feitas pelo utilizador autenticado (token com `role: 'user'`), da mais recente para a mais antiga. Inclui o nome do produto e da farmácia. Requer autenticação."
      }
    },
    "/api/reservas/{id}/concluir": {
      "patch": {
        "tags": [
          "Reservas"
        ],
        "summary": "Marcar reserva como concluída (farmácia)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Reserva marcada como concluída.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "mensagem": {
                      "type": "string",
                      "enum": [
                        "Reserva concluída."
                      ]
                    }
                  },
                  "required": [
                    "erro",
                    "mensagem"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Sem permissão para realizar esta operação.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "Conflito de estado (recurso já reservado, inativo ou duplicado).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "concluirReserva",
        "description": "A farmácia confirma que o utilizador recolheu o medicamento. Actualiza o estado da reserva para `concluida` e liberta o campo `reservado_ate` no item de stock. Requer autenticação como farmácia — só pode concluir reservas da própria farmácia.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID do recurso (farmácia, reserva, item, etc.)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/api/reservas/{id}/cancelar": {
      "patch": {
        "tags": [
          "Reservas"
        ],
        "summary": "Cancelar reserva (utilizador ou farmácia)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Reserva cancelada e stock reposto.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "mensagem": {
                      "type": "string",
                      "enum": [
                        "Reserva cancelada."
                      ]
                    }
                  },
                  "required": [
                    "erro",
                    "mensagem"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "Conflito de estado (recurso já reservado, inativo ou duplicado).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "cancelarReserva",
        "description": "Cancela uma reserva activa e repõe automaticamente o stock. Pode ser acionado pelo utilizador dono da reserva ou pela farmácia. Requer autenticação — o utilizador tem de ser o dono da reserva ou a farmácia dona do stock. Retorna erro 409 se a reserva já não estiver activa.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID do recurso (farmácia, reserva, item, etc.)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/api/reservas/{id}/aumentar": {
      "patch": {
        "tags": [
          "Reservas"
        ],
        "summary": "Aumentar quantidade de uma reserva activa (utilizador)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Reserva aumentada com sucesso.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "mensagem": {
                      "type": "string",
                      "enum": [
                        "Reserva aumentada."
                      ]
                    },
                    "quantidade": {
                      "type": "integer",
                      "description": "Quantidade total da reserva após o aumento."
                    }
                  },
                  "required": [
                    "erro",
                    "mensagem",
                    "quantidade"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Sem permissão para realizar esta operação.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "Conflito de estado (recurso já reservado, inativo ou duplicado).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "aumentarReserva",
        "description": "Adiciona unidades a uma reserva existente que esteja no estado `activa`. O stock tem de ser suficiente — caso contrário retorna 409. Requer autenticação e o utilizador tem de ser o dono da reserva.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID do recurso (farmácia, reserva, item, etc.)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "quantidade": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Número de unidades adicionais a reservar."
                  }
                },
                "required": [
                  "quantidade"
                ]
              }
            }
          }
        }
      }
    },
    "/api/perfil/me": {
      "get": {
        "tags": [
          "Perfil Farmácia"
        ],
        "summary": "Obter perfil completo da farmácia autenticada",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Perfil e stock da farmácia autenticada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeuPerfilFarmacia"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "obterMeuPerfilFarmacia",
        "description": "Retorna todos os dados do perfil da farmácia autenticada (nome, telefone, morada, horários, serviços, notas, estado, imagem, cartazes) e o array `stock` completo com todos os itens (incluindo `reservado_ate`). Requer autenticação como farmácia."
      },
      "put": {
        "tags": [
          "Perfil Farmácia"
        ],
        "summary": "Actualizar perfil da farmácia autenticada",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Perfil actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeuPerfilFarmacia"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "actualizarMeuPerfilFarmacia",
        "description": "Actualiza os dados do perfil da farmácia autenticada. O campo `nome` tem de ter pelo menos 3 caracteres e `telefone` é obrigatório. Retorna o perfil e o stock actualizados.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "nome": {
                    "type": "string",
                    "minLength": 3
                  },
                  "telefone": {
                    "type": "string",
                    "description": "Telefone principal — obrigatório para guardar."
                  },
                  "telefone_secundario": {
                    "type": "string",
                    "nullable": true
                  },
                  "endereco": {
                    "type": "string",
                    "nullable": true
                  },
                  "bairro": {
                    "type": "string",
                    "nullable": true
                  },
                  "municipio": {
                    "type": "string",
                    "nullable": true
                  },
                  "horario_abertura": {
                    "type": "string",
                    "nullable": true
                  },
                  "horario_fecho": {
                    "type": "string",
                    "nullable": true
                  },
                  "is_24_horas": {
                    "type": "boolean"
                  },
                  "servicos": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "notas": {
                    "type": "string",
                    "nullable": true
                  }
                },
                "required": [
                  "nome",
                  "telefone"
                ]
              }
            }
          }
        }
      }
    },
    "/api/perfil/stock": {
      "get": {
        "tags": [
          "Stock"
        ],
        "summary": "Listar stock da farmácia autenticada",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Array de itens de stock.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "stock": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StockItemPrivado"
                      }
                    }
                  },
                  "required": [
                    "erro",
                    "stock"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            }
          }
        },
        "operationId": "listarMeuStock",
        "description": "Retorna todos os itens de stock da farmácia autenticada, ordenados por nome. Inclui `reservado_ate` — se estiver no futuro, o item está reservado. Requer autenticação como farmácia."
      },
      "post": {
        "tags": [
          "Stock"
        ],
        "summary": "Adicionar item ao stock da farmácia",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "201": {
            "description": "Item adicionado ao stock.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "mensagem": {
                      "type": "string",
                      "enum": [
                        "Item adicionado."
                      ]
                    },
                    "item": {
                      "$ref": "#/components/schemas/StockItemPublico"
                    }
                  },
                  "required": [
                    "erro",
                    "mensagem",
                    "item"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            }
          }
        },
        "operationId": "criarItemStock",
        "description": "Cria um novo item de stock para a farmácia autenticada. O nome não pode ter mais de 150 caracteres. Requer autenticação como farmácia.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "nome": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 150,
                    "description": "Nome do medicamento ou item."
                  },
                  "categoria": {
                    "type": "string",
                    "description": "Categoria (ex.: 'Medicamento', 'Higiene')."
                  },
                  "subcategoria": {
                    "type": "string",
                    "description": "Subcategoria (ex.: 'Analgésicos', 'Vitaminas')."
                  },
                  "quantidade": {
                    "type": "integer",
                    "minimum": 0,
                    "default": 0,
                    "description": "Stock disponível."
                  },
                  "preco": {
                    "type": "number",
                    "minimum": 0,
                    "description": "Preço em Kz. Null se não definido.",
                    "nullable": true
                  }
                },
                "required": [
                  "nome",
                  "categoria",
                  "subcategoria",
                  "quantidade"
                ]
              }
            }
          }
        }
      }
    },
    "/api/perfil/stock/{id}": {
      "put": {
        "tags": [
          "Stock"
        ],
        "summary": "Actualizar um item de stock",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Item actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "mensagem": {
                      "type": "string",
                      "enum": [
                        "Item actualizado."
                      ]
                    },
                    "item": {
                      "$ref": "#/components/schemas/StockItemPublico"
                    }
                  },
                  "required": [
                    "erro",
                    "mensagem",
                    "item"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "actualizarItemStock",
        "description": "Actualiza todos os campos de um item de stock existente. A validação é idêntica à criação. Requer autenticação como farmácia — o item tem de pertencer à farmácia autenticada.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID do recurso (farmácia, reserva, item, etc.)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "nome": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 150,
                    "description": "Nome do medicamento ou item."
                  },
                  "categoria": {
                    "type": "string",
                    "description": "Categoria (ex.: 'Medicamento', 'Higiene')."
                  },
                  "subcategoria": {
                    "type": "string",
                    "description": "Subcategoria (ex.: 'Analgésicos', 'Vitaminas')."
                  },
                  "quantidade": {
                    "type": "integer",
                    "minimum": 0,
                    "default": 0,
                    "description": "Stock disponível."
                  },
                  "preco": {
                    "type": "number",
                    "minimum": 0,
                    "description": "Preço em Kz. Null se não definido.",
                    "nullable": true
                  }
                },
                "required": [
                  "nome",
                  "categoria",
                  "subcategoria",
                  "quantidade"
                ]
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Stock"
        ],
        "summary": "Remover item do stock",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Item removido do stock.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "mensagem": {
                      "type": "string",
                      "enum": [
                        "Item removido."
                      ]
                    }
                  },
                  "required": [
                    "erro",
                    "mensagem"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "apagarItemStock",
        "description": "Remove permanentemente um item de stock. Requer autenticação como farmácia — o item tem de pertencer à farmácia autenticada.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID do recurso (farmácia, reserva, item, etc.)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/api/perfil/foto": {
      "post": {
        "tags": [
          "Perfil Farmácia"
        ],
        "summary": "Actualizar foto de perfil da farmácia (multipart/form-data)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "foto": {
                    "type": "string",
                    "format": "binary",
                    "description": "Imagem de perfil (JPEG, PNG ou WebP)."
                  }
                },
                "required": [
                  "foto"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Foto actualizada com sucesso.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "mensagem": {
                      "type": "string",
                      "enum": [
                        "Foto actualizada com sucesso."
                      ]
                    },
                    "farmacia": {
                      "type": "object",
                      "properties": {
                        "imagem_url": {
                          "type": "string",
                          "format": "uri"
                        }
                      },
                      "required": [
                        "imagem_url"
                      ]
                    }
                  },
                  "required": [
                    "erro",
                    "mensagem",
                    "farmacia"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            }
          },
          "413": {
            "description": "Ficheiro maior que o limite aceite pelo servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            }
          }
        },
        "operationId": "actualizarFotoPerfil",
        "description": "Substitui a imagem de perfil da farmácia. Enviar como `multipart/form-data` com o campo `foto` (imagem JPEG/PNG/WebP). Se o R2 estiver activo, a imagem anterior é eliminada do Cloudflare R2. Requer autenticação como farmácia."
      }
    },
    "/api/perfil/acessos": {
      "get": {
        "tags": [
          "Perfil Farmácia"
        ],
        "summary": "Listar acessos recentes da farmácia (últimos 20)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Últimos 20 acessos.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "acessos": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "dispositivo": {
                            "type": "string",
                            "description": "Parsed do user-agent (ex.: 'Windows 10 / Chrome').",
                            "nullable": true
                          },
                          "navegador": {
                            "type": "string",
                            "description": "Nome do browser (ex.: 'Chrome').",
                            "nullable": true
                          },
                          "localidade": {
                            "type": "string",
                            "description": "Nome da localidade indicada no login.",
                            "nullable": true
                          },
                          "ip": {
                            "type": "string",
                            "description": "Endereço IP (pode ser IPv6).",
                            "nullable": true
                          },
                          "criado_em": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "erro",
                    "acessos"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "listarMeusAcessos",
        "description": "Retorna os últimos 20 registos de acesso à conta da farmácia (IP, user-agent, localidade e data). Útil para auditoria de segurança — se detectar acessos estranhos, contacte o suporte."
      }
    },
    "/api/denuncias": {
      "post": {
        "tags": [
          "Denúncias"
        ],
        "summary": "Criar denúncia contra utilizador (farmácia)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "201": {
            "description": "Denúncia criada com estado `pendente`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "denuncia": {
                      "$ref": "#/components/schemas/Denuncia"
                    }
                  },
                  "required": [
                    "erro",
                    "denuncia"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dados de entrada inválidos ou em falta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Sem permissão para realizar esta operação.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Recurso não encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "criarDenuncia",
        "description": "Regista uma denúncia contra um utilizador que tenha feito reserva nesta farmácia. A denúncia fica com estado `pendente` — os admins avaliam e decidem. A farmácia só pode denunciar utilizadores com reserva associada.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "utilizador_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID do utilizador a denunciar. Tem de ter uma reserva activa ou concluída nesta farmácia."
                  },
                  "motivo": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Descrição do motivo da denúncia."
                  }
                },
                "required": [
                  "utilizador_id",
                  "motivo"
                ]
              }
            }
          }
        }
      }
    },
    "/api/admin/stats": {
      "get": {
        "tags": [
          "Administração"
        ],
        "summary": "Estatísticas gerais da plataforma (admin)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Estatísticas e lista de admins.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminStats"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Sem permissão para realizar esta operação.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "obterEstatisticasAdmin",
        "description": "Retorna o número total de farmácias (activas, congeladas) e a lista de contas de administração com estado e último login. Requer papel `admin`."
      }
    },
    "/api/admin/farmacias": {
      "get": {
        "tags": [
          "Administração"
        ],
        "summary": "Listar todas as farmácias (admin — inclui inactivas)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Lista completa de farmácias (inclui inactivas).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erro": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "farmacias": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FarmaciaResumo"
                      }
                    }
                  },
                  "required": [
                    "erro",
                    "farmacias"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Token Bearer em falta, inválido ou expirado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Sem permissão para realizar esta operação.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Erro interno do servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limite de pedidos excedido (RFC 9652). Inclui header `Retry-After` (segundos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erro"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos até repetir.",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                }
              },
              "RateLimit-Policy": {
                "description": "Limite e janela em segundos: `120;w=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Estado: `limit=120, remaining=119, reset=300`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "listarFarmaciasAdmin",
        "description": "Retorna a lista completa de farmácias registadas, incluindo as congeladas/ desactivadas. O campo `is_ativo` indica o estado. Requer papel `admin`. Para activar/desactivar uma farmácia, usar `PUT /api/farmacias/{id}`."
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    },
    "schemas": {
      "Erro": {
        "type": "object",
        "required": [
          "erro",
          "code",
          "mensagem"
        ],
        "properties": {
          "erro": {
            "type": "boolean",
            "description": "Sempre true em respostas de erro.",
            "enum": [
              true
            ]
          },
          "code": {
            "type": "string",
            "description": "Código estável e legível por máquinas. Usa este campo (não a mensagem) para decidir a reação: verificar se há novo token (NAO_AUTENTICADO), esperar e repetir (LIMITE_DE_PEDIDOS), mostrar a mensagem ao utilizador, etc. Lista estável; novos códigos são publicados no changelog antes de existirem.",
            "enum": [
              "PEDIDO_INVALIDO",
              "NAO_AUTENTICADO",
              "TOKEN_EM_FALTA",
              "TOKEN_INVALIDO_OU_EXPIRADO",
              "SEM_PERMISSAO",
              "NAO_ENCONTRADO",
              "CONFLITO",
              "DEMASIADO_GRANDE",
              "TIPO_NAO_SUPORTADO",
              "VALIDACAO",
              "LIMITE_DE_PEDIDOS",
              "ERRO_INTERNO",
              "GATEWAY_INVALIDO",
              "INDISPONIVEL",
              "TIMEOUT"
            ]
          },
          "mensagem": {
            "type": "string",
            "description": "Mensagem legível em português, pronta a mostrar ao utilizador."
          },
          "detalhes": {
            "type": "object",
            "description": "Informação extra opcional (ex. campos com erro)."
          }
        },
        "additionalProperties": true,
        "example": {
          "erro": true,
          "code": "NAO_ENCONTRADO",
          "mensagem": "Item não encontrado."
        }
      },
      "FarmaciaResumo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "nome": {
            "type": "string"
          },
          "telefone": {
            "type": "string"
          },
          "telefone_secundario": {
            "type": "string",
            "nullable": true
          },
          "endereco": {
            "type": "string",
            "nullable": true
          },
          "bairro": {
            "type": "string",
            "nullable": true
          },
          "municipio": {
            "type": "string",
            "nullable": true
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          },
          "horario_abertura": {
            "type": "string",
            "description": "Hora de abertura (HH:MM)",
            "nullable": true
          },
          "horario_fecho": {
            "type": "string",
            "description": "Hora de fecho (HH:MM)",
            "nullable": true
          },
          "is_24_horas": {
            "type": "boolean"
          },
          "servicos": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "is_ativo": {
            "type": "boolean"
          },
          "imagem_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "cartazes": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "notas": {
            "type": "string",
            "nullable": true
          },
          "distancia_km": {
            "type": "number",
            "format": "double",
            "description": "Apenas quando `lat`/`lon` fornecidos — distância ao ponto de referência em km.",
            "nullable": true
          },
          "media_avaliacao": {
            "type": "number",
            "format": "float",
            "description": "Média de 1 a 5 (0 se sem avaliações)."
          },
          "total_avaliacoes": {
            "type": "integer"
          },
          "criado_em": {
            "type": "string",
            "format": "date-time"
          },
          "stock": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StockItemPublico"
            },
            "description": "Apenas preenchido quando `medicamento` é fornecido — itens do stock da farmácia que correspondem à pesquisa.",
            "nullable": true
          }
        },
        "required": [
          "id",
          "nome",
          "latitude",
          "longitude",
          "is_24_horas",
          "servicos",
          "is_ativo",
          "media_avaliacao",
          "total_avaliacoes",
          "criado_em"
        ]
      },
      "FarmaciaCompleta": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "nome": {
            "type": "string"
          },
          "telefone": {
            "type": "string"
          },
          "telefone_secundario": {
            "type": "string",
            "nullable": true
          },
          "endereco": {
            "type": "string",
            "nullable": true
          },
          "bairro": {
            "type": "string",
            "nullable": true
          },
          "municipio": {
            "type": "string",
            "nullable": true
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          },
          "horario_abertura": {
            "type": "string",
            "nullable": true
          },
          "horario_fecho": {
            "type": "string",
            "nullable": true
          },
          "is_24_horas": {
            "type": "boolean"
          },
          "servicos": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "is_ativo": {
            "type": "boolean"
          },
          "imagem_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "cartazes": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "notas": {
            "type": "string",
            "nullable": true
          },
          "media_avaliacao": {
            "type": "number",
            "format": "float"
          },
          "total_avaliacoes": {
            "type": "integer"
          },
          "criado_em": {
            "type": "string",
            "format": "date-time"
          },
          "atualizado_em": {
            "type": "string",
            "format": "date-time"
          },
          "stock": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StockItemPublico"
            }
          }
        },
        "required": [
          "id",
          "nome",
          "latitude",
          "longitude",
          "is_24_horas",
          "servicos",
          "is_ativo",
          "media_avaliacao",
          "total_avaliacoes"
        ]
      },
      "StockItemPublico": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "nome": {
            "type": "string"
          },
          "categoria": {
            "type": "string"
          },
          "subcategoria": {
            "type": "string"
          },
          "quantidade": {
            "type": "integer",
            "minimum": 0
          },
          "preco": {
            "type": "number",
            "format": "double",
            "description": "Preço em Kz. Null se não definido.",
            "nullable": true
          }
        },
        "required": [
          "id",
          "nome",
          "categoria",
          "subcategoria",
          "quantidade"
        ]
      },
      "StockItemPrivado": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "nome": {
            "type": "string"
          },
          "categoria": {
            "type": "string"
          },
          "subcategoria": {
            "type": "string"
          },
          "quantidade": {
            "type": "integer",
            "minimum": 0
          },
          "preco": {
            "type": "number",
            "format": "double",
            "description": "Preço em Kz. Null se não definido.",
            "nullable": true
          },
          "reservado_ate": {
            "type": "string",
            "format": "date-time",
            "description": "Até quando o item está reservado. Null se disponível.",
            "nullable": true
          }
        },
        "required": [
          "id",
          "nome",
          "categoria",
          "subcategoria",
          "quantidade"
        ]
      },
      "TokenResponse": {
        "type": "object",
        "properties": {
          "erro": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "mensagem": {
            "type": "string"
          },
          "token": {
            "type": "string",
            "description": "JWT Bearer. Usar no header `Authorization: Bearer <token>`."
          },
          "user": {
            "$ref": "#/components/schemas/UserInfo"
          }
        },
        "required": [
          "erro",
          "mensagem",
          "token",
          "user"
        ]
      },
      "UserInfo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "telefone": {
            "type": "string",
            "nullable": true
          },
          "role": {
            "type": "string",
            "enum": [
              "user",
              "admin"
            ]
          },
          "gestor_admins": {
            "type": "boolean",
            "description": "Apenas no login — `true` se o admin é gestor. Null ou ausente para users normais.",
            "nullable": true
          }
        }
      },
      "FarmaciaTokenResponse": {
        "type": "object",
        "properties": {
          "erro": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "mensagem": {
            "type": "string"
          },
          "token": {
            "type": "string"
          },
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              },
              "role": {
                "type": "string",
                "enum": [
                  "farmacia"
                ]
              }
            },
            "required": [
              "id",
              "name",
              "role"
            ]
          }
        },
        "required": [
          "erro",
          "mensagem",
          "token",
          "user"
        ]
      },
      "PerfilFarmacia": {
        "type": "object",
        "properties": {
          "erro": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "farmacia": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "nome": {
                "type": "string"
              },
              "telefone": {
                "type": "string"
              },
              "telefone_secundario": {
                "type": "string",
                "nullable": true
              },
              "endereco": {
                "type": "string",
                "nullable": true
              },
              "bairro": {
                "type": "string",
                "nullable": true
              },
              "municipio": {
                "type": "string",
                "nullable": true
              },
              "imagem_url": {
                "type": "string",
                "format": "uri",
                "nullable": true
              },
              "cartazes": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uri"
                }
              },
              "is_ativo": {
                "type": "boolean"
              },
              "motivo_inativo": {
                "type": "string",
                "nullable": true
              },
              "horario_abertura": {
                "type": "string",
                "nullable": true
              },
              "horario_fecho": {
                "type": "string",
                "nullable": true
              },
              "is_24_horas": {
                "type": "boolean"
              },
              "servicos": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "notas": {
                "type": "string",
                "nullable": true
              }
            },
            "required": [
              "id",
              "nome",
              "telefone",
              "is_ativo",
              "is_24_horas",
              "servicos"
            ]
          },
          "stock": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StockItemPrivado"
            }
          }
        },
        "required": [
          "erro",
          "farmacia",
          "stock"
        ]
      },
      "AvaliacaoStats": {
        "type": "object",
        "properties": {
          "media": {
            "type": "number",
            "format": "float",
            "description": "Média arredondada a 1 casa decimal."
          },
          "total": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "media",
          "total"
        ]
      },
      "AvaliacaoCriada": {
        "type": "object",
        "properties": {
          "avaliacao": {
            "type": "object",
            "description": "Avaliação criada ou actualizada.",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "farmacia_id": {
                "type": "string",
                "format": "uuid"
              },
              "utilizador_id": {
                "type": "string",
                "format": "uuid"
              },
              "rating": {
                "type": "integer",
                "enum": [
                  1,
                  2,
                  3,
                  4,
                  5
                ]
              },
              "criado_em": {
                "type": "string",
                "format": "date-time"
              }
            },
            "required": [
              "id",
              "farmacia_id",
              "utilizador_id",
              "rating"
            ]
          },
          "media": {
            "type": "number",
            "format": "float"
          },
          "total": {
            "type": "integer"
          }
        },
        "required": [
          "avaliacao",
          "media",
          "total"
        ]
      },
      "Reserva": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "utilizador_id": {
            "type": "string",
            "format": "uuid"
          },
          "farmacia_id": {
            "type": "string",
            "format": "uuid"
          },
          "stock_item_id": {
            "type": "string",
            "format": "uuid"
          },
          "quantidade": {
            "type": "integer",
            "minimum": 1
          },
          "estado": {
            "type": "string",
            "enum": [
              "activa",
              "concluida",
              "cancelada"
            ]
          },
          "reservado_ate": {
            "type": "string",
            "format": "date-time"
          },
          "criado_em": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "utilizador_id",
          "farmacia_id",
          "stock_item_id",
          "quantidade",
          "estado",
          "reservado_ate",
          "criado_em"
        ]
      },
      "ReservaComNomes": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "utilizador_id": {
            "type": "string",
            "format": "uuid"
          },
          "farmacia_id": {
            "type": "string",
            "format": "uuid"
          },
          "stock_item_id": {
            "type": "string",
            "format": "uuid"
          },
          "quantidade": {
            "type": "integer",
            "minimum": 1
          },
          "estado": {
            "type": "string",
            "enum": [
              "activa",
              "concluida",
              "cancelada"
            ]
          },
          "reservado_ate": {
            "type": "string",
            "format": "date-time"
          },
          "criado_em": {
            "type": "string",
            "format": "date-time"
          },
          "produto": {
            "type": "string",
            "description": "Nome do item em stock."
          },
          "farmacia_nome": {
            "type": "string"
          },
          "utilizador_nome": {
            "type": "string",
            "description": "Apenas em `GET /api/reservas` (vista farmácia)."
          },
          "utilizador_telefone": {
            "type": "string",
            "description": "Apenas em `GET /api/reservas` (vista farmácia).",
            "nullable": true
          }
        },
        "required": [
          "id",
          "utilizador_id",
          "farmacia_id",
          "stock_item_id",
          "quantidade",
          "estado",
          "reservado_ate",
          "criado_em"
        ]
      },
      "Denuncia": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "farmacia_id": {
            "type": "string",
            "format": "uuid"
          },
          "utilizador_id": {
            "type": "string",
            "format": "uuid"
          },
          "motivo": {
            "type": "string"
          },
          "estado": {
            "type": "string",
            "enum": [
              "pendente",
              "avaliada",
              "rejeitada"
            ]
          },
          "criado_em": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "farmacia_id",
          "utilizador_id",
          "motivo",
          "estado",
          "criado_em"
        ]
      },
      "AdminStats": {
        "type": "object",
        "properties": {
          "erro": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "estatisticas": {
            "type": "object",
            "properties": {
              "total_farmacias": {
                "type": "integer"
              },
              "total_ativas": {
                "type": "integer"
              },
              "total_congeladas": {
                "type": "integer"
              }
            },
            "required": [
              "total_farmacias",
              "total_ativas",
              "total_congeladas"
            ]
          },
          "administradores": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "nome": {
                  "type": "string"
                },
                "email": {
                  "type": "string",
                  "format": "email"
                },
                "is_ativo": {
                  "type": "boolean"
                },
                "ultimo_login": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                },
                "criado_em": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          }
        },
        "required": [
          "erro",
          "estatisticas",
          "administradores"
        ]
      },
      "MeuPerfilFarmacia": {
        "type": "object",
        "properties": {
          "erro": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "farmacia": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "nome": {
                "type": "string"
              },
              "telefone": {
                "type": "string"
              },
              "telefone_secundario": {
                "type": "string",
                "nullable": true
              },
              "endereco": {
                "type": "string",
                "nullable": true
              },
              "bairro": {
                "type": "string",
                "nullable": true
              },
              "municipio": {
                "type": "string",
                "nullable": true
              },
              "imagem_url": {
                "type": "string",
                "format": "uri",
                "nullable": true
              },
              "cartazes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "is_ativo": {
                "type": "boolean"
              },
              "motivo_inativo": {
                "type": "string",
                "nullable": true
              },
              "horario_abertura": {
                "type": "string",
                "nullable": true
              },
              "horario_fecho": {
                "type": "string",
                "nullable": true
              },
              "is_24_horas": {
                "type": "boolean"
              },
              "servicos": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "notas": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "stock": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StockItemPrivado"
            }
          }
        },
        "required": [
          "erro",
          "farmacia",
          "stock"
        ]
      }
    }
  }
}