docs: close bugs 69, 70 and 94 in the ledger

This commit is contained in:
Alexandre Possebom
2026-07-29 09:00:01 -03:00
parent 0f47c521f9
commit 7c65105557
+8 -5
View File
@@ -19,8 +19,11 @@ Como ler:
| | Crítico | Médio | Baixo | Total |
|---|---|---|---|---|
| Fechado | 3 | 29 | 55 | **87** |
| Aberto | 0 | 0 | 8 | **8** |
| Fechado | 3 | 29 | 58 | **90** |
| Aberto | 0 | 0 | 5 | **5** |
Os cinco abertos restantes: quatro esperam **decisão de produto** (75, 76, 77, 84) e um é
contrato de API (92, paginação).
Nenhum crítico nem médio em aberto: sobraram só baixos.
@@ -30,14 +33,11 @@ Nenhum crítico nem médio em aberto: sobraram só baixos.
| # | Onde | O que é | Por que importa |
|---|---|---|---|
| 69 | `models/appointment.rs::UpdateAppointment` + `repositories/appointment.rs::update` | `service_id` usa `Option` simples e `coalesce`, então `null` significa "mantém" e não há como destacar um serviço de um agendamento | Mesmo defeito do bug 53, que já usa `Option<Option<T>>` em `UpdateClient`, não aplicado aqui. `description` escapa: aceita `Some("")` |
| 70 | `controllers/upload.rs::remove_previous_avatar` (via `update_professional`) | A coluna `avatar` é gravável à mão pelo PATCH, e o próximo upload apaga `file_name()` do valor gravado | Vandalismo entre colegas da plataforma: dá para apagar o arquivo de avatar de outro. Confinado a `upload_dir` (sem travessia) e recuperável com novo upload |
| 75 | `controllers/professional.rs::get_by_id` e `::get_professional_by_email` | A existência é checada antes da autorização: id/e-mail desconhecido dá 404, de outra empresa dá 403 | Oráculo de enumeração de contas na plataforma inteira, e o `find_by_email` sobre e-mail arbitrário. Autenticado e revela só existência (mesma classe do bug 23) |
| 76 | `controllers/appointment.rs::update` | `status` só é validado contra o enum, sem máquina de estados: cancelado ou concluído volta a `confirmed`, `no_show` pode ser marcado antes da hora | A `appointments_no_overlap` barra reconfirmar sobre horário retomado (23P01 → 400); sobra a inconsistência de estado (faturamento, no-show). Único gate é `require_read_company` |
| 77 | `controllers/client.rs::update` | Exige só `require_read_company`, enquanto `delete_by_id` exige `require_manage_company` | Profissional comum reescreve nome/telefone/e-mail de qualquer cliente da empresa. Mas `create` usa a mesma régua de leitura, então pode ser política de balcão deliberada: **decisão de produto** (ver 84) |
| 84 | `controllers/appointment.rs::delete` (cancelar) | Exige só `require_read_company`, enquanto a listagem por profissional exige `manage` | Qualquer profissional cancela agendamento de colega. Mas o comentário do `update` declara a política permissiva de propósito, então o furo real é contra a listagem: **decisão de produto** (ver 77) |
| 92 | `repositories/appointment.rs::get_appointments_by_professional_id` | `SELECT * FROM appointments WHERE professional_id = $1`, sem filtro de data, paginação ou ordenação | Cresce para sempre. Mesma classe do bug 19, fechado só para o caminho de conflito |
| 94 | `controllers/upload.rs::upload` | O arquivo é escrito antes do `update_professional`, e o `return Err` do UPDATE sai sem recolhê-lo | Falha no UPDATE deixa arquivo órfão. O nome é determinístico por dia, então o próximo upload sobrescreve |
## Fechados
@@ -110,6 +110,8 @@ Nenhum crítico nem médio em aberto: sobraram só baixos.
| 66 | Médio | Refresh vazado valia 5 dias, se auto-renovava para sempre e trocar a senha não cortava nada. Dois cortes stateless: `password_changed_at` (coluna nova, bump no UPDATE de senha) recusa todo token com `issued_at` anterior, no refresh e no extractor (mesmo RETURNING, sem ida extra ao banco); e a claim `login_at` viaja intacta pela cadeia de renovação, com teto de 30 dias do login original. Deploy invalida todo token antigo, precedente do bug 4. Verificado com servidor real | `aaba575` |
| 67 | Médio | Nome de serviço sem checagem contra o `varchar(80)` (500 no nome longo) e update sem checar nome nenhum (`{"name":""}` apagava em silêncio um nome da página pública). Limite conferido em caracteres, não bytes (acento é comum em nome de serviço), e vazio no update significa "mantém" | `0438941` |
| 68 | Baixo | `min_lead_minutes`/`max_horizon_days` só tinham CHECK de sinal, e ~95 milhões de dias estouravam `now + horizon` em `available_slots`, derrubando todo cálculo de slot da empresa; create e update agora limitam a 1 ano de horizonte e 1 semana de antecedência | `cd1cabf` |
| 69 | Baixo | `service_id` do PATCH de agendamento usava `Option` simples com coalesce: `null` significava "mantém" e não havia como destacar um serviço. Agora é merge padrão via `double_option` genérico compartilhado (mesmo padrão do e-mail de Client, bug 53): ausente mantém, `null` destaca (vira encaixe), valor troca com as validações de sempre; verificado com servidor real | `0f47c52` |
| 70 | Baixo | A coluna `avatar` era gravável à mão pelo corpo do PATCH (vandalismo entre colegas: o próximo upload apagava o arquivo do valor gravado); o campo virou `skip_deserializing`, só o handler de upload escreve | `0f47c52` |
| 71 | Baixo | O claim de lembretes sem `LIMIT` marcava o backlog inteiro como enviado antes de qualquer e-mail sair: restart no meio do laço serial perdia o resto pra sempre. Agora reivindica no máximo 50 por vez (`REMINDER_CLAIM_BATCH`, o teto da perda num crash) e o `sweep` drena lote a lote: cheio pede outro, não-cheio encerra sem ida extra ao banco | `f87955e` |
| 72 | Baixo | Slug já tomado no `create_company` caía em `Err(_) => 500`, descartando o `Conflict` do classificador; agora responde o mesmo 400 do update | `7ca1fcd` |
| 73 | Baixo | Qualquer falha do `get_company_by_id` respondia "empresa não existe", banco fora do ar incluso; agora só `NotFound` é 404 e infra é 500 | `7ca1fcd` |
@@ -128,6 +130,7 @@ Nenhum crítico nem médio em aberto: sobraram só baixos.
| 90 | Baixo | E-mail de login case-sensitive: `Alexandre@` não logava na conta `alexandre@` e podia virar segunda conta. Linhas existentes minusculadas por migration (que falha alto se já houver duplicata de caixa), índice único trocado por `lower(email)`, create/update gravam a forma canônica e o login compara `lower()` dos dois lados; verificado com servidor real | `6d6f413` |
| 91 | Baixo | O `compute_slots` carregava a semana inteira e filtrava o weekday em Rust apesar do índice `(professional_id, weekday)`; o filtro desceu ao SQL (`get_working_hours_for_weekday`), e o GET da grade mantém a query da semana que realmente precisa; verificado com servidor real (8 slots da janela de 4h) | `6dc68b2` |
| 93 | Baixo | O upload lia só o primeiro campo do multipart: formulário com campo de texto antes do arquivo levava 400 "Missing file name"; campos de texto agora são pulados até o campo de arquivo, e multipart sem arquivo nenhum responde "a file field is required" | `865e47d` |
| 94 | Baixo | Falha do UPDATE depois de o arquivo do avatar já estar gravado deixava um órfão no disco até o upload do dia seguinte sobrescrever; o caminho de erro agora o recolhe | `0f47c52` |
| 95 | Baixo | `"@"` sozinho passava como e-mail na marcação pública e entrava em toda notificação; o mínimo agora é parte local não-vazia e domínio com ponto interno, e o nome (sem limite no schema) ganhou teto de 100 caracteres | `865e47d` |
| 96 | Baixo | Mover profissional para empresa inexistente batia na FK (`StillReferenced`) e respondia 500; o update ganhou o mesmo braço de 400 que o create já tinha | `7ca1fcd` |
| 98 | Baixo | Irmão do 58 pela outra porta: PATCH que troca só o `professional_id` não conferia se o novo profissional oferece o serviço do agendamento; a validação agora cobre o par resultante, inclusive quando só o profissional muda | `f9acbff` |