From 510b988a385dcdea833a72f3372b61a2e56a5c55 Mon Sep 17 00:00:00 2001 From: Returner_org Date: Wed, 1 Jul 2026 04:16:53 +0300 Subject: [PATCH] =?UTF-8?q?docs(errors):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B8=D0=BD=D0=BB=D0=B0=D0=B9=D0=BD-?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BE=20=D0=B2=D1=81=D0=B5=D0=BC=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=D0=BC=20ErrorCode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/elexam_core/errors.py | 228 ++++++++++++++++---------------------- 1 file changed, 96 insertions(+), 132 deletions(-) diff --git a/src/elexam_core/errors.py b/src/elexam_core/errors.py index f81e2ef..4e05d79 100644 --- a/src/elexam_core/errors.py +++ b/src/elexam_core/errors.py @@ -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] = {