docs(errors): добавить инлайн-комментарии ко всем кодам ErrorCode

This commit is contained in:
Returner_org 2026-07-01 04:16:53 +03:00
parent fd4af8e103
commit 510b988a38

View File

@ -5,158 +5,122 @@ class ErrorCode(str, Enum):
# --- common ---
# Универсальные коды для случаев, когда доменный код избыточен
COMMON_INTERNAL_ERROR = "common.internal_error"
COMMON_SERVICE_UNAVAILABLE = "common.service_unavailable"
COMMON_VALIDATION_FAILED = "common.validation_failed"
COMMON_NOT_FOUND = "common.not_found"
COMMON_FORBIDDEN = "common.forbidden"
COMMON_UNAUTHORIZED = "common.unauthorized"
COMMON_RATE_LIMITED = "common.rate_limited"
COMMON_CONFLICT = "common.conflict"
COMMON_METHOD_NOT_ALLOWED = "common.method_not_allowed"
COMMON_PAYLOAD_TOO_LARGE = "common.payload_too_large"
COMMON_INTERNAL_ERROR = "common.internal_error" # Непредвиденная ошибка на сервере — используй, когда нет подходящего доменного кода
COMMON_SERVICE_UNAVAILABLE = "common.service_unavailable" # Зависимый сервис (RabbitMQ, Redis, БД) временно недоступен
COMMON_VALIDATION_FAILED = "common.validation_failed" # Тело запроса не прошло схемную валидацию Pydantic
COMMON_NOT_FOUND = "common.not_found" # Запрошенный ресурс не найден — резервный код, если нет доменного аналога
COMMON_FORBIDDEN = "common.forbidden" # Прав доступа недостаточно для выполнения операции
COMMON_UNAUTHORIZED = "common.unauthorized" # Запрос без токена или с нераспознанным токеном
COMMON_RATE_LIMITED = "common.rate_limited" # Клиент превысил допустимое число запросов в единицу времени
COMMON_CONFLICT = "common.conflict" # Конфликт состояния: операция несовместима с текущим состоянием ресурса
COMMON_METHOD_NOT_ALLOWED = "common.method_not_allowed" # HTTP-метод не поддерживается для данного эндпоинта
COMMON_PAYLOAD_TOO_LARGE = "common.payload_too_large" # Тело запроса превышает допустимый размер
# --- auth ---
AUTH_TOKEN_EXPIRED = "auth.token.expired"
AUTH_TOKEN_INVALID = "auth.token.invalid"
AUTH_TOKEN_REVOKED = "auth.token.revoked"
AUTH_CREDENTIALS_INVALID = "auth.credentials.invalid"
AUTH_ACCOUNT_LOCKED = "auth.account.locked"
AUTH_ACCOUNT_DEPRECATED = "auth.account.deprecated"
AUTH_DEVICE_UNTRUSTED = "auth.device.untrusted"
AUTH_DEVICE_NOT_FOUND = "auth.device.not_found"
AUTH_REFRESH_EXPIRED = "auth.refresh.expired"
AUTH_REFRESH_REVOKED = "auth.refresh.revoked"
# Применяется, когда организация заморожена gateway'ем по событию billing
AUTH_ORG_SUSPENDED = "auth.org.suspended"
# Порог брутфорса достигнут — фиксируется журналом + rate-limit-ом
AUTH_BRUTE_FORCE = "auth.brute_force"
AUTH_TOKEN_EXPIRED = "auth.token.expired" # JWT access-токен просрочен (exp пройден) — клиент должен обновить через refresh
AUTH_TOKEN_INVALID = "auth.token.invalid" # Подпись токена невалидна или структура JWT нарушена
AUTH_TOKEN_REVOKED = "auth.token.revoked" # Токен явно отозван (logout или смена пароля)
AUTH_CREDENTIALS_INVALID = "auth.credentials.invalid" # Неверный логин или пароль при аутентификации
AUTH_ACCOUNT_LOCKED = "auth.account.locked" # Аккаунт заблокирован администратором вручную
AUTH_ACCOUNT_DEPRECATED = "auth.account.deprecated" # Аккаунт деактивирован (soft-delete) — вход запрещён
AUTH_DEVICE_UNTRUSTED = "auth.device.untrusted" # Устройство не добавлено в список доверенных (MFA-сценарий)
AUTH_DEVICE_NOT_FOUND = "auth.device.not_found" # Указанный device_id не зарегистрирован в системе
AUTH_REFRESH_EXPIRED = "auth.refresh.expired" # Refresh-токен просрочен — требуется повторный вход
AUTH_REFRESH_REVOKED = "auth.refresh.revoked" # Refresh-токен отозван (ротация или принудительный logout)
AUTH_ORG_SUSPENDED = "auth.org.suspended" # Применяется, когда организация заморожена gateway'ем по событию billing
AUTH_BRUTE_FORCE = "auth.brute_force" # Порог брутфорса достигнут — фиксируется журналом + rate-limit-ом
# --- user ---
USER_NOT_FOUND = "user.not_found"
USER_EMAIL_TAKEN = "user.email.taken"
USER_DEPRECATED = "user.deprecated"
USER_ORG_NOT_FOUND = "user.org.not_found"
USER_ORG_DEPRECATED = "user.org.deprecated"
USER_BRANCH_NOT_FOUND = "user.branch.not_found"
USER_MEMBERSHIP_EXISTS = "user.membership.exists"
USER_MEMBERSHIP_NOT_FOUND = "user.membership.not_found"
USER_ROLE_NOT_FOUND = "user.role.not_found"
USER_PERMISSION_NOT_FOUND = "user.permission.not_found"
USER_NOT_FOUND = "user.not_found" # Пользователь с данным ID не существует
USER_EMAIL_TAKEN = "user.email.taken" # Email уже зарегистрирован в системе — дубликат
USER_DEPRECATED = "user.deprecated" # Аккаунт пользователя деактивирован (soft-delete)
USER_ORG_NOT_FOUND = "user.org.not_found" # Организация, к которой привязан пользователь, не найдена
USER_ORG_DEPRECATED = "user.org.deprecated" # Организация деактивирована — операции для её участников заблокированы
USER_BRANCH_NOT_FOUND = "user.branch.not_found" # Указанное подразделение (филиал) организации не найдено
USER_MEMBERSHIP_EXISTS = "user.membership.exists" # Пользователь уже состоит в данной организации или роли
USER_MEMBERSHIP_NOT_FOUND = "user.membership.not_found" # Запись членства пользователя в организации не найдена
USER_ROLE_NOT_FOUND = "user.role.not_found" # Указанная роль не существует в системе
USER_PERMISSION_NOT_FOUND = "user.permission.not_found" # Указанное право доступа не зарегистрировано в системе
# --- exam ---
EXAM_NOT_FOUND = "exam.not_found"
EXAM_ALREADY_PUBLISHED = "exam.already_published"
EXAM_CLOSED = "exam.closed"
# Нельзя опубликовать черновик без единого вопроса
EXAM_DRAFT_EMPTY = "exam.draft.empty"
EXAM_VERSION_NOT_FOUND = "exam.version.not_found"
EXAM_QUESTION_NOT_FOUND = "exam.question.not_found"
EXAM_QUESTION_DUPLICATE = "exam.question.duplicate"
EXAM_ATTEMPT_NOT_FOUND = "exam.attempt.not_found"
# Попытка уже запущена — двойной старт невозможен
EXAM_ATTEMPT_ALREADY_STARTED = "exam.attempt.already_started"
# Студент исчерпал максимум попыток (exam.settings.attempts.max)
EXAM_ATTEMPT_MAX_EXCEEDED = "exam.attempt.max_exceeded"
# Серверный дедлайн истёк до сабмита
EXAM_ATTEMPT_TIMED_OUT = "exam.attempt.timed_out"
# Попытка аннулирована по вердикту прокторинга
EXAM_ATTEMPT_VOIDED = "exam.attempt.voided"
# Попытка уже закрыта (submitted) — нельзя добавлять ответы
EXAM_ATTEMPT_SUBMITTED = "exam.attempt.submitted"
# Старт заблокирован: нет факта согласия (proctoring.consent_given)
EXAM_ATTEMPT_CONSENT_REQUIRED = "exam.attempt.consent_required"
# Повторная попытка раньше cooldown_seconds
EXAM_ATTEMPT_COOLDOWN_ACTIVE = "exam.attempt.cooldown_active"
EXAM_NOT_FOUND = "exam.not_found" # Экзамен с данным ID не найден
EXAM_ALREADY_PUBLISHED = "exam.already_published" # Экзамен уже опубликован — повторная публикация невозможна
EXAM_CLOSED = "exam.closed" # Экзамен закрыт для новых попыток (дедлайн прошёл или ручное закрытие)
EXAM_DRAFT_EMPTY = "exam.draft.empty" # Нельзя опубликовать черновик без единого вопроса
EXAM_VERSION_NOT_FOUND = "exam.version.not_found" # Запрошенная immutable-версия (снимок) экзамена не найдена
EXAM_QUESTION_NOT_FOUND = "exam.question.not_found" # Вопрос с данным ID не входит в состав экзамена
EXAM_QUESTION_DUPLICATE = "exam.question.duplicate" # Вопрос уже добавлен в экзамен — дублирование запрещено
EXAM_ATTEMPT_NOT_FOUND = "exam.attempt.not_found" # Попытка с данным ID не найдена или не принадлежит студенту
EXAM_ATTEMPT_ALREADY_STARTED = "exam.attempt.already_started" # Попытка уже запущена — двойной старт невозможен
EXAM_ATTEMPT_MAX_EXCEEDED = "exam.attempt.max_exceeded" # Студент исчерпал максимум попыток (exam.settings.attempts.max)
EXAM_ATTEMPT_TIMED_OUT = "exam.attempt.timed_out" # Серверный дедлайн истёк до сабмита
EXAM_ATTEMPT_VOIDED = "exam.attempt.voided" # Попытка аннулирована по вердикту прокторинга
EXAM_ATTEMPT_SUBMITTED = "exam.attempt.submitted" # Попытка уже закрыта (submitted) — нельзя добавлять ответы
EXAM_ATTEMPT_CONSENT_REQUIRED = "exam.attempt.consent_required" # Старт заблокирован: нет факта согласия (proctoring.consent_given)
EXAM_ATTEMPT_COOLDOWN_ACTIVE = "exam.attempt.cooldown_active" # Повторная попытка раньше cooldown_seconds
# --- proctoring ---
PROCTORING_CONSENT_ALREADY_GIVEN = "proctoring.consent.already_given"
PROCTORING_CONSENT_NOT_FOUND = "proctoring.consent.not_found"
PROCTORING_SESSION_NOT_FOUND = "proctoring.session.not_found"
PROCTORING_SESSION_ALREADY_ACTIVE = "proctoring.session.already_active"
# Сессия уже закрыта (closed) — новые сигналы не принимаются
PROCTORING_SESSION_CLOSED = "proctoring.session.closed"
# Сигнал не прошёл валидацию схемы (неизвестный type, невалидный payload)
PROCTORING_SIGNAL_REJECTED = "proctoring.signal.rejected"
PROCTORING_INCIDENT_NOT_FOUND = "proctoring.incident.not_found"
# Вердикт по этой сессии уже вынесен (один вердикт на сессию — аудит)
PROCTORING_VERDICT_ALREADY_ISSUED = "proctoring.verdict.already_issued"
PROCTORING_VERDICT_NOT_FOUND = "proctoring.verdict.not_found"
# Снимок для ручной идентификации не загружен при открытии сессии
PROCTORING_IDENTITY_SHOT_MISSING = "proctoring.identity_shot.missing"
# Клиент пытается слать данные по каналу (напр. mic), на который не давал согласия
PROCTORING_CHANNEL_NOT_CONSENTED = "proctoring.channel.not_consented"
PROCTORING_CONSENT_ALREADY_GIVEN = "proctoring.consent.already_given" # Студент уже подписал согласие для данной попытки
PROCTORING_CONSENT_NOT_FOUND = "proctoring.consent.not_found" # Запись согласия не найдена — студент ещё не давал согласие
PROCTORING_SESSION_NOT_FOUND = "proctoring.session.not_found" # Сессия прокторинга с данным ID не найдена
PROCTORING_SESSION_ALREADY_ACTIVE = "proctoring.session.already_active" # Для данной попытки уже открыта активная сессия прокторинга
PROCTORING_SESSION_CLOSED = "proctoring.session.closed" # Сессия уже закрыта (closed) — новые сигналы не принимаются
PROCTORING_SIGNAL_REJECTED = "proctoring.signal.rejected" # Сигнал не прошёл валидацию схемы (неизвестный type, невалидный payload)
PROCTORING_INCIDENT_NOT_FOUND = "proctoring.incident.not_found" # Инцидент с данным ID не найден в сессии
PROCTORING_VERDICT_ALREADY_ISSUED = "proctoring.verdict.already_issued" # Вердикт по этой сессии уже вынесен (один вердикт на сессию — аудит)
PROCTORING_VERDICT_NOT_FOUND = "proctoring.verdict.not_found" # Вердикт для данной сессии ещё не вынесен
PROCTORING_IDENTITY_SHOT_MISSING = "proctoring.identity_shot.missing" # Снимок для ручной идентификации не загружен при открытии сессии
PROCTORING_CHANNEL_NOT_CONSENTED = "proctoring.channel.not_consented" # Клиент пытается слать данные по каналу (напр. mic), на который не давал согласия
# --- result ---
RESULT_NOT_FOUND = "result.not_found"
# Результат аннулирован — изменения невозможны
RESULT_VOIDED = "result.voided"
# Итог уже финализирован — повторная финализация запрещена
RESULT_ALREADY_FINALIZED = "result.already_finalized"
# Финализация невозможна: остались непроверенные grading-записи
RESULT_PENDING_GRADING = "result.pending_grading"
RESULT_GRADING_NOT_FOUND = "result.grading.not_found"
# Вопрос уже оценён — повторная оценка без апелляции запрещена
RESULT_GRADING_ALREADY_DONE = "result.grading.already_done"
# Апелляции отключены в настройках экзамена (appeals.enabled = false)
RESULT_APPEAL_NOT_ALLOWED = "result.appeal.not_allowed"
RESULT_APPEAL_NOT_FOUND = "result.appeal.not_found"
# По этому результату уже открыта апелляция (одна активная)
RESULT_APPEAL_ALREADY_OPEN = "result.appeal.already_open"
# Апелляция уже закрыта (accepted/rejected)
RESULT_APPEAL_CLOSED = "result.appeal.closed"
RESULT_NOT_FOUND = "result.not_found" # Результат попытки с данным ID не найден
RESULT_VOIDED = "result.voided" # Результат аннулирован — изменения невозможны
RESULT_ALREADY_FINALIZED = "result.already_finalized" # Итог уже финализирован — повторная финализация запрещена
RESULT_PENDING_GRADING = "result.pending_grading" # Финализация невозможна: остались непроверенные grading-записи
RESULT_GRADING_NOT_FOUND = "result.grading.not_found" # Запись оценки для данного вопроса не найдена
RESULT_GRADING_ALREADY_DONE = "result.grading.already_done" # Вопрос уже оценён — повторная оценка без апелляции запрещена
RESULT_APPEAL_NOT_ALLOWED = "result.appeal.not_allowed" # Апелляции отключены в настройках экзамена (appeals.enabled = false)
RESULT_APPEAL_NOT_FOUND = "result.appeal.not_found" # Апелляция для данного результата не найдена
RESULT_APPEAL_ALREADY_OPEN = "result.appeal.already_open" # По этому результату уже открыта апелляция (одна активная)
RESULT_APPEAL_CLOSED = "result.appeal.closed" # Апелляция уже закрыта (accepted/rejected)
# --- notification ---
NOTIFICATION_NOT_FOUND = "notification.not_found"
NOTIFICATION_TEMPLATE_NOT_FOUND = "notification.template.not_found"
# Канал (email/telegram) отключён настройками пользователя
NOTIFICATION_CHANNEL_DISABLED = "notification.channel.disabled"
# Внешний провайдер (SMTP / Telegram API) вернул ошибку
NOTIFICATION_DELIVERY_FAILED = "notification.delivery.failed"
# Уведомление по этому event_id уже существует (идемпотентный дедуп)
NOTIFICATION_DUPLICATE = "notification.duplicate"
NOTIFICATION_PREF_NOT_FOUND = "notification.pref.not_found"
NOTIFICATION_NOT_FOUND = "notification.not_found" # Уведомление с данным ID не найдено
NOTIFICATION_TEMPLATE_NOT_FOUND = "notification.template.not_found" # Шаблон уведомления (по типу события) не зарегистрирован
NOTIFICATION_CHANNEL_DISABLED = "notification.channel.disabled" # Канал (email/telegram) отключён настройками пользователя
NOTIFICATION_DELIVERY_FAILED = "notification.delivery.failed" # Внешний провайдер (SMTP / Telegram API) вернул ошибку
NOTIFICATION_DUPLICATE = "notification.duplicate" # Уведомление по этому event_id уже существует (идемпотентный дедуп)
NOTIFICATION_PREF_NOT_FOUND = "notification.pref.not_found" # Настройки уведомлений пользователя не найдены (ещё не сохранялись)
# --- billing ---
BILLING_PLAN_NOT_FOUND = "billing.plan.not_found"
BILLING_SUBSCRIPTION_NOT_FOUND = "billing.subscription.not_found"
BILLING_SUBSCRIPTION_ALREADY_EXISTS = "billing.subscription.already_exists"
# Подписка истекла и не продлена (период закончился)
BILLING_SUBSCRIPTION_EXPIRED = "billing.subscription.expired"
# Организация заморожена (suspended) — операции недоступны
BILLING_SUBSCRIPTION_SUSPENDED = "billing.subscription.suspended"
# Лимит тарифа исчерпан (напр. max_concurrent_attempts)
BILLING_LIMIT_EXCEEDED = "billing.limit.exceeded"
BILLING_INVOICE_NOT_FOUND = "billing.invoice.not_found"
# Платёжный провайдер отклонил транзакцию
BILLING_PAYMENT_FAILED = "billing.payment.failed"
BILLING_PLAN_NOT_FOUND = "billing.plan.not_found" # Тарифный план с данным ID не существует
BILLING_SUBSCRIPTION_NOT_FOUND = "billing.subscription.not_found" # Активная подписка для организации не найдена
BILLING_SUBSCRIPTION_ALREADY_EXISTS = "billing.subscription.already_exists" # Организация уже имеет подписку — создание дубликата запрещено
BILLING_SUBSCRIPTION_EXPIRED = "billing.subscription.expired" # Подписка истекла и не продлена (период закончился)
BILLING_SUBSCRIPTION_SUSPENDED = "billing.subscription.suspended" # Организация заморожена (suspended) — операции недоступны
BILLING_LIMIT_EXCEEDED = "billing.limit.exceeded" # Лимит тарифа исчерпан (напр. max_concurrent_attempts)
BILLING_INVOICE_NOT_FOUND = "billing.invoice.not_found" # Счёт на оплату с данным ID не найден
BILLING_PAYMENT_FAILED = "billing.payment.failed" # Платёжный провайдер отклонил транзакцию
# --- bot ---
BOT_LINK_NOT_FOUND = "bot.link.not_found"
# Пользователь уже привязан к Telegram-чату
BOT_LINK_ALREADY_EXISTS = "bot.link.already_exists"
BOT_LINK_CODE_NOT_FOUND = "bot.link_code.not_found"
# Одноразовый код привязки истёк (expires_at пройден)
BOT_LINK_CODE_EXPIRED = "bot.link_code.expired"
# Код уже был активирован — повторное использование невозможно
BOT_LINK_CODE_USED = "bot.link_code.used"
# Telegram Bot API вернул ошибку при отправке
BOT_DELIVERY_FAILED = "bot.delivery.failed"
BOT_LINK_NOT_FOUND = "bot.link.not_found" # Привязка Telegram-аккаунта к пользователю не найдена
BOT_LINK_ALREADY_EXISTS = "bot.link.already_exists" # Пользователь уже привязан к Telegram-чату
BOT_LINK_CODE_NOT_FOUND = "bot.link_code.not_found" # Одноразовый код привязки не найден или был удалён
BOT_LINK_CODE_EXPIRED = "bot.link_code.expired" # Одноразовый код привязки истёк (expires_at пройден)
BOT_LINK_CODE_USED = "bot.link_code.used" # Код уже был активирован — повторное использование невозможно
BOT_DELIVERY_FAILED = "bot.delivery.failed" # Telegram Bot API вернул ошибку при отправке
# --- storage ---
STORAGE_FILE_NOT_FOUND = "storage.file.not_found"
STORAGE_UPLOAD_TOO_LARGE = "storage.upload.too_large"
STORAGE_TYPE_NOT_ALLOWED = "storage.type.not_allowed"
# Квота хранилища организации исчерпана (billing.limits.storage_gb)
STORAGE_QUOTA_EXCEEDED = "storage.quota.exceeded"
# Хранилище недоступно или вернуло ошибку при записи
STORAGE_UPLOAD_FAILED = "storage.upload.failed"
STORAGE_FILE_NOT_FOUND = "storage.file.not_found" # Файл с данным ID не найден в хранилище
STORAGE_UPLOAD_TOO_LARGE = "storage.upload.too_large" # Размер загружаемого файла превышает допустимый лимит
STORAGE_TYPE_NOT_ALLOWED = "storage.type.not_allowed" # MIME-тип загружаемого файла не входит в список разрешённых
STORAGE_QUOTA_EXCEEDED = "storage.quota.exceeded" # Квота хранилища организации исчерпана (billing.limits.storage_gb)
STORAGE_UPLOAD_FAILED = "storage.upload.failed" # Хранилище недоступно или вернуло ошибку при записи
# --- journal ---
JOURNAL_AUDIT_NOT_FOUND = "journal.audit.not_found"
JOURNAL_STATS_NOT_FOUND = "journal.stats.not_found"
# Запрошенный экспорт слишком велик для синхронной выдачи
JOURNAL_EXPORT_TOO_LARGE = "journal.export.too_large"
JOURNAL_AUDIT_NOT_FOUND = "journal.audit.not_found" # Запись аудит-лога с данным ID не найдена
JOURNAL_STATS_NOT_FOUND = "journal.stats.not_found" # Статистика по запрошенным параметрам ещё не сформирована
JOURNAL_EXPORT_TOO_LARGE = "journal.export.too_large" # Запрошенный экспорт слишком велик для синхронной выдачи
ERROR_META: dict[ErrorCode, dict] = {