package httpapi import ( "encoding/json" "errors" "io" "net/http" "strconv" "strings" "time" "unicode" "unicode/utf8" "botpontos/internal/adapters/pontomais" "botpontos/internal/app" "botpontos/internal/domain" "botpontos/internal/httpx" ) const maxBodyBytes = 64 << 10 // decode lê o corpo JSON recusando campos desconhecidos. func decode(w http.ResponseWriter, r *http.Request, out any) bool { r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() if err := dec.Decode(out); err != nil { title := "Corpo da requisição inválido" if errors.Is(err, io.EOF) { title = "Corpo da requisição vazio" } httpx.WriteProblem(w, http.StatusBadRequest, title, "BAD_REQUEST") return false } return true } func pathID(w http.ResponseWriter, r *http.Request) (int64, bool) { id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil || id <= 0 { httpx.WriteProblem(w, http.StatusBadRequest, "Identificador inválido", "BAD_REQUEST") return 0, false } return id, true } func queryString(r *http.Request, key string) *string { v := strings.TrimSpace(r.URL.Query().Get(key)) if v == "" { return nil } return &v } func queryInt64(r *http.Request, key string) (*int64, error) { v := queryString(r, key) if v == nil { return nil, nil } n, err := strconv.ParseInt(*v, 10, 64) if err != nil { return nil, domain.ValidationErrors{{Field: key, Message: "número inválido"}} } return &n, nil } func queryBool(r *http.Request, key string) (*bool, error) { v := queryString(r, key) if v == nil { return nil, nil } b, err := strconv.ParseBool(*v) if err != nil { return nil, domain.ValidationErrors{{Field: key, Message: "use true ou false"}} } return &b, nil } // queryDate lê AAAA-MM-DD; end=true devolve o início do dia seguinte (limite exclusivo). func queryDate(r *http.Request, key string, loc *time.Location, end bool) (*time.Time, error) { v := queryString(r, key) if v == nil { return nil, nil } t, err := time.ParseInLocation(domain.DateLayout, *v, loc) if err != nil { return nil, domain.ValidationErrors{{Field: key, Message: "data inválida (use AAAA-MM-DD)"}} } if end { t = t.AddDate(0, 0, 1) } return &t, nil } // writeError traduz os erros do domínio em respostas problem+json. func (a *API) writeError(w http.ResponseWriter, err error) { var validation domain.ValidationErrors var credErr *app.CredentialError switch { case errors.As(err, &validation): httpx.WriteProblemDetails(w, httpx.Problem{ Title: "Dados inválidos", Status: http.StatusUnprocessableEntity, Code: "VALIDATION", Errors: validation, }) case errors.As(err, &credErr): if errors.Is(err, pontomais.ErrAuthRejected) { httpx.WriteProblem(w, http.StatusUnprocessableEntity, "A PontoMais recusou o e-mail ou a senha; a credencial anterior foi mantida", "CREDENTIALS_REJECTED") return } httpx.WriteProblem(w, http.StatusBadGateway, "Não foi possível validar na PontoMais agora; a credencial anterior foi mantida", "POINTS_UNAVAILABLE") case errors.Is(err, domain.ErrNotFound): httpx.WriteProblem(w, http.StatusNotFound, "Registro não encontrado", "NOT_FOUND") case errors.Is(err, domain.ErrCredentialsRequired): httpx.WriteProblem(w, http.StatusConflict, "Configure e valide a credencial da PontoMais antes", "CREDENTIALS_REQUIRED") case errors.Is(err, domain.ErrUserNotActive): httpx.WriteProblem(w, http.StatusConflict, "O usuário precisa estar ativo", "USER_NOT_ACTIVE") case errors.Is(err, domain.ErrLastOwner): httpx.WriteProblem(w, http.StatusConflict, "É preciso manter pelo menos um owner ativo", "LAST_OWNER") case errors.Is(err, domain.ErrInvalidState): httpx.WriteProblem(w, http.StatusConflict, capitalize(err.Error()), "INVALID_STATE") case errors.Is(err, domain.ErrInvalidLogin): httpx.WriteProblem(w, http.StatusUnauthorized, "Usuário ou senha inválidos", "INVALID_LOGIN") case errors.Is(err, domain.ErrAccountLocked): httpx.WriteProblem(w, http.StatusLocked, "Conta bloqueada por tentativas de login; tente mais tarde", "ACCOUNT_LOCKED") case errors.Is(err, domain.ErrNotConfigured): httpx.WriteProblem(w, http.StatusServiceUnavailable, "Integração não configurada no servidor", "NOT_CONFIGURED") case errors.Is(err, pontomais.ErrAuthRejected): httpx.WriteProblem(w, http.StatusBadGateway, "A PontoMais recusou a credencial gravada", "CREDENTIALS_REJECTED") case isPointsError(err): httpx.WriteProblem(w, http.StatusBadGateway, "A PontoMais não respondeu; tente novamente", "POINTS_UNAVAILABLE") default: a.log.Error("erro interno na API", "error", err) httpx.WriteProblem(w, http.StatusInternalServerError, "Erro interno", "INTERNAL") } } func isPointsError(err error) bool { return strings.Contains(err.Error(), "PontoMais") } func capitalize(s string) string { first, size := utf8.DecodeRuneInString(s) if first == utf8.RuneError { return s } return string(unicode.ToUpper(first)) + s[size:] } func badRequest(field, message string) error { return domain.ValidationErrors{{Field: field, Message: message}} }