package repository import ( "errors" "fmt" "strings" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" ) // Классы ошибок, которые понимают handlers. // // ErrNotFound / ErrConflict / ErrInvalid — ошибки предметной области, // им можно отдавать клиенту код и сообщение. // Всё остальное — инфраструктура (сеть, авторизация, падение БД): // наружу уходит только «internal server error», детали пишутся в лог. var ( // ErrNotFound — сущность не найдена → 404. ErrNotFound = errors.New("not found") // ErrConflict — нарушение уникальности или конфликт состояния → 409. ErrConflict = errors.New("conflict") // ErrInvalid — нарушение ограничений целостности (FK, CHECK, NOT NULL) → 422. ErrInvalid = errors.New("invalid") // Ниже — специализированные конфликты приглашений (миграция 0005). // Каждый заворачивается в ErrConflict (→ 409), но хендлеру даёт // точный текст для клиента: «уже состоит» и «ждём ответа» — разные // ситуации, хотя обе это 409. // // ErrMembershipActive — мастер уже активно состоит в проекте. ErrMembershipActive = errors.New("worker already active in project") // ErrInvitationPending — приглашение отправлено и ещё не принято. ErrInvitationPending = errors.New("invitation already pending") // ErrInvitationExpired — срок ответа истёк, приглашение мертво. ErrInvitationExpired = errors.New("invitation expired") // ErrInvitationNotPending — строка есть, но ждать решения уже нечего // (мастер ответил или строка устарела на момент операции). ErrInvitationNotPending = errors.New("invitation is not pending") // ErrResetToken — токен сброса пароля неизвестен, уже использован или // истёк. Наружу уходит одним текстом (400 invalid token): различать // причины — помощь переборщику. ErrResetToken = errors.New("invalid reset token") // ErrUserHasData — аккаунт нельзя удалить: на него ссылается история — // заявки, проекты или участия (FK без каскада). Хендлер отвечает 409 // и предлагает отключить аккаунт вместо удаления — историю не стираем. ErrUserHasData = errors.New("user has related records") // ErrAlreadyConfirmed — подтверждать уже нечего: аккаунт подтверждён // раньше (повторный POST /users/:id/confirm). Хендлер ветвится сам // и отвечает 409 already confirmed. ErrAlreadyConfirmed = errors.New("already confirmed") // Ниже — специализированные конфликты фотоотчёта (миграция 0008). // Оба заворачиваются в ErrConflict (→ 409), но хендлеру дают // точный текст: «слишком много файлов» и «нельзя удалить» — // разные ситуации, хотя обе это 409. ErrTooManyPhotos = errors.New("too many photos") // ErrPhotoNotRemovable — чужое фото или заявка уже закрыта. ErrPhotoNotRemovable = errors.New("photo cannot be removed") // Ниже — рубежи правила повторяющейся заявки: фоновый планировщик // пропускает такое правило молча (создания заявки нет), а ручной // запуск (POST /recurring/:id/run) получает конфликт с причиной. // Каждый заворачивается в ErrConflict (→ 409). ErrRuleNotActive = errors.New("rule is not active") ErrRuleOpen = errors.New("open ticket exists") ErrProjectNotActive = errors.New("project is not active") ErrObjectNotActive = errors.New("object is not active") ) // classify превращает ошибку pgx в ошибку одного из трёх классов. // Инфраструктурные ошибки возвращаются как есть, чтобы handlers не // приняли обрыв соединения за «запись не нашлась». // Контекст вызывающего («ticket 12») сохраняется — он нужен для лога, // наружу он не уходит: handlers отдают только фиксированные тексты. func classify(err error) error { if err == nil { return nil } var pgErr *pgconn.PgError if errors.As(err, &pgErr) { switch { case pgErr.Code == pgUniqueViolation: // 23505 return fmt.Errorf("%w: %w", err, ErrConflict) case strings.HasPrefix(pgErr.Code, "23"): // 23xxx — integrity constraint return fmt.Errorf("%w: %w", err, ErrInvalid) } // Прочие коды (08xxx сеть, 40001 deadlock, 42xxx права) — инфраструктура. return err } if errors.Is(err, pgx.ErrNoRows) { return fmt.Errorf("%w: %w", err, ErrNotFound) } return err } const ( pgUniqueViolation = "23505" pgFKViolation = "23503" ) // notFound — обёртка, чтобы сообщение об ошибке было информативным в логе. func notFound(format string, args ...any) error { return fmt.Errorf("%w: %s", ErrNotFound, fmt.Sprintf(format, args...)) }