# Validação de dados e tratamento de erros — rotas de client

Documento de registro do trabalho de padronização da **validação de
requisições** e do **tratamento de erros** nas rotas usadas pelo *client*
(candidatos). As rotas de *backoffice* (OPERATOR / ADMINISTRATOR) **não são
alteradas** — ficam como estão.

> Data: 2026-05-22

---

## 1. Objetivos

1. **Nunca retornar erros crus do Prisma para o client.** Todo `catch` de
   controller de client deve gerar uma mensagem amigável, como já é feito em
   `control-candidate.ts`.
2. **Validar todas as requisições de client** de acordo com o que o schema
   Prisma (`prisma/schema.prisma`) aceita: obrigatoriedade, tipos, limites de
   tamanho (`VarChar`), enums documentados, datas e UUIDs.
3. Manter as rotas de backoffice inalteradas.

---

## 2. Classificação das rotas

### 2.1 Rotas de CLIENT (candidato) — alvo deste trabalho

| Método | Rota | Controller |
|--------|------|------------|
| POST | `/candidate` | `candidates/create-candidate.ts` |
| POST | `/candidate/change-password` | `candidates/change-password.ts` |
| GET | `/candidate/cpf/:cpf` | `candidates/get-candidate.ts` |
| GET | `/candidate/login/:login/exists` | `candidates/exists-candidate.ts` |
| GET | `/candidate/:id/answers/specific` | `candidates/get-candidate-specific-answers.ts` |
| GET | `/candidate/:id/answers/general` | `candidates/get-candidate-general-answers.ts` |
| GET | `/candidate/:id_candidate/curriculum` | `candidates/curriculum/get-curriculum.ts` |
| POST/PUT/DELETE | `/candidate/curriculum/*` | `candidates/curriculum/{create,update,delete}-curriculum.ts` |
| GET/POST/DELETE | `/candidate/:id_candidate/profile-photo` | `candidates/curriculum/profile-photo.ts` |
| POST | `/candidate/auth` e fluxo de senha | `auth/candidate.ts`, `auth/forgot-password.ts`, `auth/reset-password.ts`, `auth/validate-reset-token.ts` |
| POST | `/answers` | `answers/create-answer.ts` |
| PUT | `/answers/:id` | `answers/update-answer.ts` |
| POST | `/candidatures` | `candidatures/create-candidature.ts` |
| GET | `/candidatures` / `/candidatures/:id` | `candidatures/list-candidatures.ts`, `candidatures/get-candidature.ts` |
| GET | `/vacancies`, `/vacancies/:id` | `vacancies/list-vacancies.ts`, `vacancies/get-vacancy.ts` (públicas, sem auth) |

### 2.2 Rotas de BACKOFFICE — **não alteradas**

Todas as rotas protegidas por `verifyRole(["OPERATOR", "ADMINISTRATOR"])`:
gestão de vagas (criar/editar/pausar), usuários, departamentos, benefícios,
avaliações, feedbacks, perguntas, listagem de candidatos, etapas de vaga e
ações de candidatura (aprovar/reprovar/próxima etapa). Mantidas como estão.

---

## 3. Problemas encontrados

### P1 — `try/catch` em `control-candidate.ts` mascara a causa real
`ControlCandidateUseCase.create()` envolve a verificação de duplicidade e o
`throw new Error("Usuário já existe.")` no mesmo `try`. O `catch` captura
inclusive esse erro de regra de negócio e o substitui por
`"Não foi possível criar o usuário."`, escondendo a mensagem correta do
client. Erros reais do Prisma (ex.: violação de `@unique`) também caem no
genérico sem distinção.

**Correção:** o `throw` de negócio deve ocorrer fora do `catch` que o
mascara; o `catch` deve tratar apenas erros inesperados (Prisma) com mensagem
amigável.

### P2 — Erros do Prisma podem vazar para o client
Vários controllers de client têm o fallback
`if (error instanceof Error) return reply.status(400).send({ message: error.message })`.
Como `PrismaClientKnownRequestError` **é** uma `Error`, sua `message` —
verbosa, com nome de coluna, SQL e modelo — é enviada direto ao client.
Rotas afetadas: `change-password`, `get-candidate`, `exists-candidate`,
curriculum (`get`/`create`/`update`/`delete`), `profile-photo`,
`get-candidate-*-answers`, `create/get/list-candidature`, `create/update-answer`,
`list-vacancies`, `get-vacancy`.

**Correção:** helper centralizado `handlePrismaError` em `src/error/` que
converte `PrismaClientKnownRequestError` / `PrismaClientValidationError` em
mensagem amigável; usado no `catch` dos controllers de client.

### P3 — Schemas Zod não refletem os limites do banco
Campos cujo banco impõe limite/forma não são validados na entrada:

| Campo | Banco | Validação atual |
|-------|-------|-----------------|
| `Answer.text` / `question` | `@db.Text` | OK (limitado por `size`) |
| `Candidature.status` | enum `REPROVED\|APPROVED\|HIRED\|ANALYSIS` | OK no create |
| `Education.education_level` | enum documentado | apenas `string().min(1)` |
| `Education.status` | enum `CURSANDO\|CONCLUIDO\|TRANCADO` | apenas `string().min(1)` |
| `Experience.start_date` | `DateTime` | `coerce.date()` sem checar data inválida |
| `Language.level` | valor controlado | apenas `string().min(1)` |
| `PersonalData` strings | `String` (255 no MySQL) | sem limite máximo |

**Correção:** reforçar os schemas Zod das rotas de client com:
- enums documentados no `schema.prisma` (`education_level`, `status`);
- limite máximo de caracteres compatível com colunas `String` (255);
- validação de data real (`coerce.date()` rejeitando `Invalid Date`).

### P4 — `auth/reset-password.ts` engole todo erro como "Token inválido"
O `catch` único responde sempre `401 Token inválido ou expirado`, mesmo se a
falha for do Prisma ao atualizar a senha. O client recebe mensagem enganosa.

**Correção:** distinguir erro de token (401) de erro de persistência
(mensagem amigável genérica), sem vazar Prisma.

---

## 4. Mudanças aplicadas

| # | Arquivo | Mudança |
|---|---------|---------|
| 1 | `src/error/handle-prisma-error.ts` (novo) | Helper que mapeia erros conhecidos do Prisma para mensagens amigáveis. |
| 2 | `src/use-cases/control-candidate.ts` | `create()` deixa de mascarar o erro de negócio; `catch` trata só o inesperado. |
| 3 | Controllers de client | `catch` passa a usar `handlePrismaError` antes do fallback genérico. |
| 4 | Schemas Zod de client | Enums, limites de tamanho e validação de data conforme o banco. |

> As rotas de backoffice não foram tocadas.

---

## 5. Como o helper funciona

`handlePrismaError(error)` retorna `{ status, message }` quando reconhece o
erro, ou `null` caso contrário. Mapeamento:

- `P2002` (unique constraint) → `409` "Registro já existe."
- `P2025` (registro não encontrado) → `404` "Registro não encontrado."
- `P2003` (foreign key) → `400` "Referência inválida."
- `PrismaClientValidationError` → `400` "Dados inválidos."
- demais erros do Prisma → `500` "Erro ao processar a solicitação."

Nos controllers, o `catch` fica:

```ts
} catch (error) {
  if (error instanceof z.ZodError)
    return reply.status(400).send({ errors: error.issues.map((e) => e.message) });

  const prismaError = handlePrismaError(error);
  if (prismaError)
    return reply.status(prismaError.status).send({ message: prismaError.message });

  if (error instanceof Error)
    return reply.status(400).send({ message: error.message });

  return reply.status(400).send({ message: "Erro na requisição" });
}
```

---

## 6. Arquivos efetivamente alterados

**Novos:**
- `src/error/handle-prisma-error.ts` — helper de tratamento de erro Prisma.
- `src/schemas/curriculum-enums.ts` — enums e limites espelhando o banco.

**Use case:**
- `src/use-cases/control-candidate.ts` — `create()` não mascara mais o erro
  de negócio.

**Controllers de client (tratamento de erro Prisma + ajustes de validação):**
- `candidates/`: `create-candidate`, `change-password`, `get-candidate`,
  `exists-candidate`, `get-candidate-specific-answers`,
  `get-candidate-general-answers` (estes dois passaram a exigir `id` UUID).
- `candidates/curriculum/`: `create-curriculum`, `update-curriculum`,
  `delete-curriculum`, `get-curriculum`, `profile-photo`.
- `answers/`: `create-answer` (`id_reference` agora UUID), `update-answer`.
- `candidatures/`: `create-candidature`, `get-candidature`, `list-candidatures`.
- `vacancies/`: `list-vacancies`, `get-vacancy` (rotas públicas de client).
- `auth/`: `candidate`, `forgot-password`, `reset-password` (este último
  passou a distinguir falha de token de falha de persistência).

---

## 7. Verificação

- `tsc --noEmit`: comparado o resultado antes (via `git stash`) e depois das
  mudanças — **a lista de erros é idêntica**. Todos os erros reportados são
  pré-existentes (uniões `Success | Fail` sem discriminante em `auth/*`, path
  errado em `control-benefit.ts`, `moduleResolution` em `vite.config.ts`).
  Nenhum erro novo foi introduzido; nenhum arquivo alterado neste trabalho
  apresenta erro de tipo.
- `vitest run`: os 12 arquivos de teste **já falhavam antes** das mudanças —
  o `vitest.config.ts` mapeia o alias `src` mas os specs importam com `@/`,
  que não está configurado. É uma falha de configuração pré-existente, não
  relacionada a este trabalho.

---

## 8. Validação caso a caso (POST e DELETE)

Refinamento das mensagens de validação para que cada campo e cada `id` de
rota de client retorne um erro próprio, em vez de mensagem genérica do Zod:

- **POST currículo** (`create-curriculum.ts`): cada campo (`type`, `subtype`,
  `language`, `level`, `company`, `role`, `activities`, `study_area`,
  `institution`, `course`) ganhou mensagem de obrigatoriedade própria e teto
  de tamanho (`DB_STRING_MAX`).
- **PUT currículo** (`update-curriculum.ts`): mesmas mensagens, na variante
  "não pode ficar vazio(a)" (campo opcional, mas se enviado não pode ser
  vazio).
- **DELETE / PUT currículo**: o parâmetro `id` passou a usar uma fábrica de
  schema por entidade (`idParamsSchema(entity)`), com mensagem como
  "ID de formação inválido."; o 404 de recurso inexistente também é
  descritivo ("Nenhum(a) idioma encontrado(a).").
- **POST/PUT answers**: `question`/`text` ganharam teto `@db.Text` (65535);
  `size` ganhou validação de inteiro positivo com teto e mensagens próprias.
- **POST candidatura**: `id_vacancy` e `status` ganharam mensagens próprias.
- Os rótulos das entidades do currículo foram centralizados em
  `ENTITY_LABELS` (`src/schemas/curriculum-enums.ts`).

Conferido que **todos** os arquivos de controller alterados possuem fallback
genérico em cada `catch` (`"Erro na requisição"`). Exceção justificada:
`auth/reset-password.ts` — o `catch` externo responde
`401 "Token inválido ou expirado."`, que é o fallback adequado para esse
contexto (a persistência tem `try/catch` interno com fallback próprio).

---

## 9. Verificação do front-end

Front em `gerteltalentos/src`. Como os erros chegam ao candidato:

- **`services/request.ts` → `reqCandidate`**: para status `>= 400`, lança
  `new Error(data.errors.join(" ") ?? data.message ?? "Erro N: ...")`. Ou
  seja, **as mensagens `message` e `errors` que o backend retorna são
  exatamente o que o front usa** — `errors` (Zod) tem prioridade, depois
  `message` (helper Prisma / regra de negócio). ✅
- **`RecoverPassword.tsx`** e **delete de conta**: leem `response.data.message`
  direto (usam `api` com `validateStatus: () => true`). A mensagem do backend
  passa. ✅

### Correções aplicadas no front

Os pontos abaixo foram **corrigidos** para que o candidato veja as mensagens
descritivas do backend:

1. **`catch {}` que descartavam o erro** — substituídos por
   `catch (err) { toast.error(err instanceof Error ? err.message : fallback) }`
   nos handlers de escrita de candidato:
   - `CandidateDashboard.tsx`: trocar senha, deletar conta.
   - `JobOpening.tsx`: salvar currículo, salvar respostas (handler
     `handleSaveAnswers` e o fluxo principal de candidatura), enviar
     candidatura, login.
   - `LoginHeader.tsx`: login do candidato.
   Os `catch {}` de carregamento secundário/opcional (foto de perfil,
   perguntas, respostas prévias) foram mantidos — degradação graciosa, não
   exibem erro ao usuário.
2. **Payload divergente em `JobOpening.tsx` (`handleSaveAnswers`)** —
   corrigido para enviar `{ id_question, question, text, type,
   id_reference, size }`, alinhado ao que o backend valida. Antes enviava
   `{ id_candidature, id_question, answer }`, que falhava sempre.
3. **Parsing do erro cru do Prisma no cadastro** — `JobOpening.tsx` fazia
   `message.includes("candidates_email_key")` (nome de índice MySQL) para
   distinguir e-mail de CPF duplicado. Como o backend agora **nunca expõe o
   erro cru**, isso deixaria de funcionar. Corrigido em duas pontas:
   - **Backend** (`control-candidate.ts`): `create()` lança mensagens
     distintas — `"Este CPF já possui cadastro."` e
     `"Este e-mail já está em uso."`.
   - **Front**: o cadastro passou a tratar `status >= 400` e exibir
     `errors[0] ?? message` direto, sem depender de nome de índice.

**Conclusão:** após estas correções, o candidato vê as mensagens definidas
neste trabalho em todas as telas de escrita (cadastro, login, currículo,
respostas, candidatura, troca de senha, exclusão de conta). Front verificado
com `tsc -b` (sem erros) e `eslint` (apenas 1 warning pré-existente de
`exhaustive-deps`, não relacionado).
