Рефакторинг (этап 1): валидаторы и клавиатуры вынесены в модули

bot.py разгружен: чистые функции вынесены без изменения поведения:
- validators.py — регэкспы, normalize_phone, format_phone_display, is_valid_name
- keyboards.py — все get_*_keyboard

Поведение проверено регрессионным прогоном (полный флоу заявки, админка, меню).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kenik
2026-06-24 21:41:21 +03:00
parent 30a02cf09b
commit ba0f43169c
3 changed files with 363 additions and 349 deletions
+44
View File
@@ -0,0 +1,44 @@
"""Валидация и форматирование пользовательского ввода."""
import re
from typing import Optional
# Регулярные выражения для валидации
# Российский номер для нормализации 8XXX / 7XXX -> +7XXX
RU_PHONE_REGEX = re.compile(r"^\+?[78]\d{10}$")
# Международный формат (E.164): + и 10–15 цифр. Принимаем и иностранные номера.
INTL_PHONE_REGEX = re.compile(r"^\+?\d{10,15}$")
# Имя: буквы (кириллица/латиница), пробел, дефис, апостроф; длина 2–50
NAME_REGEX = re.compile(r"^[A-Za-zА-Яа-яЁё][A-Za-zА-Яа-яЁё \-']{1,49}$")
def normalize_phone(text: str) -> Optional[str]:
"""Очищает и нормализует телефон. Возвращает нормализованный номер или None, если формат неверен."""
phone_cleaned = re.sub(r"[\s\-\(\)]", "", text)
# Российские номера приводим к виду +7XXXXXXXXXX
if phone_cleaned.startswith("8") and len(phone_cleaned) == 11:
phone_cleaned = "+7" + phone_cleaned[1:]
elif phone_cleaned.startswith("7") and len(phone_cleaned) == 11:
phone_cleaned = "+" + phone_cleaned
# Принимаем как российский, так и любой международный номер в формате E.164
if RU_PHONE_REGEX.match(phone_cleaned) or INTL_PHONE_REGEX.match(phone_cleaned):
return phone_cleaned
return None
def format_phone_display(phone: str) -> str:
"""Красивое отображение номера: +7 (999) 123-45-67 для РФ, иначе как есть."""
if not phone:
return phone
digits = re.sub(r"\D", "", phone)
if len(digits) == 11 and digits[0] in ("7", "8"):
d = digits
return f"+7 ({d[1:4]}) {d[4:7]}-{d[7:9]}-{d[9:11]}"
return phone
def is_valid_name(text: str) -> bool:
"""Имя должно состоять только из букв, пробела, дефиса или апострофа (без цифр и спецсимволов)."""
return bool(NAME_REGEX.match(text.strip()))