Implement payment confirmation flow and improve order navigation - #8
Conversation
Руководство для ревьюераЭтот PR интегрирует полный процесс отслеживания бонусов и подтверждения платежей — хранение и выдачу предоплатных/полных бонусов, расширение административных действий для подтверждения платежей и уведомления пользователей — а также перерабатывает диалог создания заказа, вводя механизм навигации назад на каждом шаге, при этом отображая информацию о платежах и бонусах в профиле, списках заказов и детальных представлениях. Диаграмма последовательности подтверждения платежа администратором и выдачи бонусовsequenceDiagram
actor Admin
participant Bot
participant "ORDERS (storage)"
participant "BONUSES (storage)"
participant User
Admin->>Bot: Confirm prepayment/full payment for order
Bot->>"ORDERS (storage)": Update order payment fields
Bot->>"BONUSES (storage)": Release bonus for user
Bot->>User: Notify about payment confirmation and bonus credited
Bot->>"ORDERS (storage)": Save updated order
Bot->>"BONUSES (storage)": Save updated bonus
Диаграмма сущность-связь для бонусов и заказовerDiagram
USER ||--o{ ORDER : places
USER ||--o{ BONUS : has
ORDER {
string order_id
string payment_state
boolean prepayment_confirmed
boolean full_payment_confirmed
datetime prepayment_confirmed_at
datetime full_payment_confirmed_at
int bonus_total
int bonus_released_prepaid
int bonus_released_full
}
BONUS {
int balance
string[] history
}
Диаграмма классов для логики подтверждения бонусов и платежейclassDiagram
class Order {
+order_id: str
+payment_state: str
+prepayment_confirmed: bool
+full_payment_confirmed: bool
+prepayment_confirmed_at: datetime
+full_payment_confirmed_at: datetime
+bonus_total: int
+bonus_released_prepaid: int
+bonus_released_full: int
}
class BonusEntry {
+balance: int
+history: list
}
class Bot {
+ensure_order_payment_fields(order: dict): bool
+release_bonus(user_id: str, order: dict, stage: str): int
+get_user_bonus_balance(user_id: str): int
+get_pending_bonus(user_id: str): int
}
Order "1" -- "*" BonusEntry : releases bonus
Bot "1" -- "*" Order : manages
Bot "1" -- "*" BonusEntry : manages
Изменения на уровне файлов
Советы и командыВзаимодействие с Sourcery
Настройка вашего опытаПолучите доступ к вашей панели управления, чтобы:
Получение помощи
Original review guide in EnglishReviewer's GuideThis PR integrates a full bonus tracking and payment confirmation flow—storing and releasing prepayment/full bonuses, extending admin actions to confirm payments and notify users—and overhauls the order creation conversation by introducing a back-navigation mechanism at each step, while surfacing payment and bonus information in profile, order listings, and detail views. Sequence diagram for admin payment confirmation and bonus releasesequenceDiagram
actor Admin
participant Bot
participant "ORDERS (storage)"
participant "BONUSES (storage)"
participant User
Admin->>Bot: Confirm prepayment/full payment for order
Bot->>"ORDERS (storage)": Update order payment fields
Bot->>"BONUSES (storage)": Release bonus for user
Bot->>User: Notify about payment confirmation and bonus credited
Bot->>"ORDERS (storage)": Save updated order
Bot->>"BONUSES (storage)": Save updated bonus
Entity relationship diagram for bonuses and orderserDiagram
USER ||--o{ ORDER : places
USER ||--o{ BONUS : has
ORDER {
string order_id
string payment_state
boolean prepayment_confirmed
boolean full_payment_confirmed
datetime prepayment_confirmed_at
datetime full_payment_confirmed_at
int bonus_total
int bonus_released_prepaid
int bonus_released_full
}
BONUS {
int balance
string[] history
}
Class diagram for bonus and payment confirmation logicclassDiagram
class Order {
+order_id: str
+payment_state: str
+prepayment_confirmed: bool
+full_payment_confirmed: bool
+prepayment_confirmed_at: datetime
+full_payment_confirmed_at: datetime
+bonus_total: int
+bonus_released_prepaid: int
+bonus_released_full: int
}
class BonusEntry {
+balance: int
+history: list
}
class Bot {
+ensure_order_payment_fields(order: dict): bool
+release_bonus(user_id: str, order: dict, stage: str): int
+get_user_bonus_balance(user_id: str): int
+get_pending_bonus(user_id: str): int
}
Order "1" -- "*" BonusEntry : releases bonus
Bot "1" -- "*" Order : manages
Bot "1" -- "*" BonusEntry : manages
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Привет! Я ознакомился с твоими изменениями, и они выглядят отлично!
Подсказка для AI-агентов
Пожалуйста, учти комментарии из этого обзора кода:
## Индивидуальные комментарии
### Comment 1
<location> `bot.py:656-667` </location>
<code_context>
+ return await prompt_contact_input(update, context, via_callback=True)
+ return await prompt_contact_input(update, context)
async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
contact = update.message.text.strip()
+ if contact == BACK_BUTTON_TEXT:
+ await update.message.reply_text("Возвращаемся к файлам.", reply_markup=ReplyKeyboardRemove())
+ return await prompt_file_upload(update, context, from_back=True)
if not contact:
await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
return INPUT_CONTACT
context.user_data['current_contact'] = contact
context.user_data['last_contact'] = contact
+ await update.message.reply_text("Контакт сохранен.", reply_markup=ReplyKeyboardRemove())
return await add_upsell(update, context)
+async def back_from_topic(update: Update, context: ContextTypes.DEFAULT_TYPE):
</code_context>
<issue_to_address>
**предложение:** Валидация контактов может быть строже, чтобы избежать неверных записей.
Рассмотри возможность реализации валидации, чтобы убедиться, что контакты соответствуют ожидаемым форматам, таким как ники Telegram, адреса электронной почты или ссылки VK.
```suggestion
import re
def is_valid_contact(contact: str) -> bool:
# Telegram handle: starts with @, 5-32 chars, letters, numbers, underscores
telegram_pattern = r"^@[A-Za-z0-9_]{5,32}$"
# Email address
email_pattern = r"^[\w\.-]+@[\w\.-]+\.\w{2,}$"
# VK link: vk.com/id123456 or vk.com/username
vk_pattern = r"^(https?://)?(www\.)?vk\.com/(id\d+|[A-Za-z0-9_\.]+)$"
return (
re.match(telegram_pattern, contact)
or re.match(email_pattern, contact)
or re.match(vk_pattern, contact)
)
async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
contact = update.message.text.strip()
if contact == BACK_BUTTON_TEXT:
await update.message.reply_text("Возвращаемся к файлам.", reply_markup=ReplyKeyboardRemove())
return await prompt_file_upload(update, context, from_back=True)
if not contact:
await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
return INPUT_CONTACT
if not is_valid_contact(contact):
await update.message.reply_text(
"Пожалуйста, укажите корректный контакт: Telegram (@username), email, или ссылку на VK.",
reply_markup=ReplyKeyboardRemove()
)
return INPUT_CONTACT
context.user_data['current_contact'] = contact
context.user_data['last_contact'] = contact
await update.message.reply_text("Контакт сохранен.", reply_markup=ReplyKeyboardRemove())
return await add_upsell(update, context)
```
</issue_to_address>
### Comment 2
<location> `bot.py:1220-1229` </location>
<code_context>
+ return InlineKeyboardMarkup(buttons)
+
# Обработчик админ-меню
async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
</code_context>
<issue_to_address>
**проблема (риск_бага):** Обработчикам подтверждения оплаты администратором не хватает защиты от параллельного доступа при записи файлов.
Одновременные действия администраторов могут привести к повреждению данных из-за параллельных записей. Реализуйте блокировку файлов или атомарные операции для обеспечения безопасного доступа.
</issue_to_address>
### Comment 3
<location> `bot.py:488-498` </location>
<code_context>
+ keyboard = [[InlineKeyboardButton(f"{val['icon']} {val['name']}", callback_data=f'type_{key}')] for key, val in ORDER_TYPES.items()]
+ keyboard.append([InlineKeyboardButton("⬅️ Меню", callback_data='back_to_main')])
+ reply_markup = InlineKeyboardMarkup(keyboard)
+ if query:
+ try:
+ await query.edit_message_text(text, reply_markup=reply_markup)
+ except TelegramError as e:
+ if "message is not modified" in str(e).lower():
+ pass
+ else:
+ raise
+ else:
+ await update.message.reply_text(text, reply_markup=reply_markup)
+ return SELECT_ORDER_TYPE
# Подробности о типе заказа
</code_context>
<issue_to_address>
**предложение:** Обработчик `select_order_type` должен обрабатывать неожиданные типы обновлений.
Рассмотрите возможность добавления обработки ошибок для случаев, когда отсутствует как `callback query`, так и `message`, чтобы предотвратить неожиданные сбои.
```suggestion
if query:
try:
await query.edit_message_text(text, reply_markup=reply_markup)
except TelegramError as e:
if "message is not modified" in str(e).lower():
pass
else:
raise
elif getattr(update, "message", None):
await update.message.reply_text(text, reply_markup=reply_markup)
else:
# Handle unexpected update type
logger.error("select_order_type: Neither callback_query nor message present in update: %s", update)
return ConversationHandler.END
return SELECT_ORDER_TYPE
```
</issue_to_address>
### Comment 4
<location> `bot.py:119` </location>
<code_context>
def release_bonus(user_id: str, order: dict, stage: str) -> int:
ensure_order_payment_fields(order)
user_key = str(user_id)
bonus_entry = BONUSES.setdefault(user_key, {'balance': 0, 'history': []})
amount = 0
if stage == 'prepayment':
if order.get('bonus_released_prepaid'):
return 0
amount = order.get('bonus_total', 0) // 2
order['bonus_released_prepaid'] = amount
elif stage == 'full':
if order.get('bonus_released_full'):
return 0
already = order.get('bonus_released_prepaid', 0)
amount = max(order.get('bonus_total', 0) - already, 0)
order['bonus_released_full'] = amount
if amount <= 0:
return 0
bonus_entry['balance'] = bonus_entry.get('balance', 0) + amount
history = bonus_entry.setdefault('history', [])
history.append({
'order_id': order.get('order_id'),
'amount': amount,
'stage': stage,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
return amount
</code_context>
<issue_to_address>
**проблема (качество_кода):** Обнаружены следующие проблемы:
- Удалите ненужные приведения к int, str, float или bool ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))
- Упростите условное выражение до формы switch [×2] ([`switch`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/switch/))
</issue_to_address>
### Comment 5
<location> `bot.py:146` </location>
<code_context>
def get_user_bonus_balance(user_id: str) -> int:
entry = BONUSES.get(str(user_id), {})
return int(entry.get('balance', 0))
</code_context>
<issue_to_address>
**предложение (качество_кода):** Удалите ненужные приведения к int, str, float или bool ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))
```suggestion
entry = BONUSES.get(user_id, {})
```
</issue_to_address>
### Comment 6
<location> `bot.py:152` </location>
<code_context>
def get_pending_bonus(user_id: str) -> int:
total = 0
changed = False
for order in ORDERS.get(str(user_id), []):
if ensure_order_payment_fields(order):
changed = True
credited = order.get('bonus_released_prepaid', 0) + order.get('bonus_released_full', 0)
total += max(order.get('bonus_total', 0) - credited, 0)
if changed:
save_json(ORDERS_FILE, ORDERS)
return int(total)
</code_context>
<issue_to_address>
**предложение (качество_кода):** Удалите ненужные приведения к int, str, float или bool ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))
```suggestion
for order in ORDERS.get(user_id, []):
```
</issue_to_address>
### Comment 7
<location> `bot.py:192` </location>
<code_context>
def build_order_details(uid: str, order: dict) -> str:
changed = ensure_order_payment_fields(order)
if changed:
save_json(ORDERS_FILE, ORDERS)
order_id = order.get('order_id', 'N/A')
order_name = ORDER_TYPES.get(order.get('type'), {}).get('name', order.get('type', 'Неизвестно'))
user_link = escape(f"tg://user?id={uid}", quote=True)
lines = [
f"<b>Заказ #{order_id}</b>",
f"Пользователь: <a href=\"{user_link}\">{escape(str(uid))}</a>",
f"Тип: {escape(order_name)}",
f"Тема: {escape(order.get('topic', 'Без темы'))}",
f"Срок: {order.get('deadline_days', 'N/A')} дней",
f"Статус: {escape(order.get('status', 'неизвестно'))}",
f"Оплата: {escape(order.get('payment_state', 'не оплачен'))}",
f"Контакт: {format_contact_link(order.get('contact'))}",
f"Требования: {escape(order.get('requirements', 'Нет'))}",
]
if order.get('upsells'):
upsells_readable = ', '.join(UPSELL_TITLES.get(code, code) for code in order['upsells'])
lines.append(f"Допы: {escape(upsells_readable)}")
else:
lines.append("Допы: нет")
lines.append(f"Файлов: {len(order.get('attachments') or [])}")
bonus_total = order.get('bonus_total', 0)
if bonus_total:
lines.append(
f"Бонусы: всего {bonus_total} ₽ | предоплата {order.get('bonus_released_prepaid', 0)} ₽ | оплата {order.get('bonus_released_full', 0)} ₽"
)
if order.get('prepayment_confirmed_at'):
lines.append(f"Предоплата подтверждена: {escape(order['prepayment_confirmed_at'])}")
if order.get('full_payment_confirmed_at'):
lines.append(f"Оплата подтверждена: {escape(order['full_payment_confirmed_at'])}")
if order.get('created_at'):
lines.append(f"Создан: {escape(order['created_at'])}")
return '<br>'.join(lines)
</code_context>
<issue_to_address>
**проблема (качество_кода):** Обнаружены следующие проблемы:
- Используйте именованные выражения для упрощения присваивания и условного выражения [×2] ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
- Удалите ненужные приведения к int, str, float или bool ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))
</issue_to_address>
### Comment 8
<location> `bot.py:492-494` </location>
<code_context>
async def select_order_type(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await answer_callback(query)
data = query.data if query else None
user = update.effective_user
log_user_action(user.id, user.username, "Выбор типа заказа")
if data == 'back_to_main':
return await main_menu(update, context)
text = "Выберите тип работы (добавьте несколько в корзину для скидки!):"
keyboard = [[InlineKeyboardButton(f"{val['icon']} {val['name']}", callback_data=f'type_{key}')] for key, val in ORDER_TYPES.items()]
keyboard.append([InlineKeyboardButton("⬅️ Меню", callback_data='back_to_main')])
reply_markup = InlineKeyboardMarkup(keyboard)
if query:
try:
await query.edit_message_text(text, reply_markup=reply_markup)
except TelegramError as e:
if "message is not modified" in str(e).lower():
pass
else:
raise
else:
await update.message.reply_text(text, reply_markup=reply_markup)
return SELECT_ORDER_TYPE
</code_context>
<issue_to_address>
**предложение (качество_кода):** Поменяйте местами if/else, чтобы убрать пустой блок if ([`remove-pass-body`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-pass-body/))
```suggestion
if "message is not modified" not in str(e).lower():
```
</issue_to_address>
### Comment 9
<location> `bot.py:1070` </location>
<code_context>
async def show_orders(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await answer_callback(query)
data = query.data
if data == 'profile':
return await show_profile(update, context)
user_id = str(update.effective_user.id)
user_orders = ORDERS.get(user_id, [])
if not user_orders:
text = "Пока нет заказов. Сделайте заказ сейчас!"
else:
text = "Ваши заказы:\n"
changed = False
for order in user_orders:
name = ORDER_TYPES.get(order.get('type'), {}).get('name', 'Неизвестно')
if ensure_order_payment_fields(order):
changed = True
payment_state = order.get('payment_state', 'не оплачен')
text += f"#{order.get('order_id', 'N/A')}: {name} - {order.get('status', 'новый')} | Оплата: {payment_state}\n"
if changed:
save_json(ORDERS_FILE, ORDERS)
keyboard = [[InlineKeyboardButton("Назад", callback_data='profile')]]
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
return SHOW_ORDERS
</code_context>
<issue_to_address>
**проблема (качество_кода):** Обнаружены следующие проблемы:
- Используйте именованное выражение для упрощения присваивания и условного выражения ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
- Поменяйте местами ветви if/else ([`swap-if-else-branches`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/swap-if-else-branches/))
</issue_to_address>
### Comment 10
<location> `bot.py:1157-1160` </location>
<code_context>
def build_admin_order_keyboard(uid: str, order_id_str: str, order: dict) -> InlineKeyboardMarkup:
buttons = []
if not order.get('prepayment_confirmed'):
buttons.append([
InlineKeyboardButton("Подтвердить предоплату", callback_data=f'admin_confirm_prepay|{uid}|{order_id_str}')
])
if not order.get('full_payment_confirmed'):
buttons.append([
InlineKeyboardButton("Подтвердить оплату", callback_data=f'admin_confirm_full|{uid}|{order_id_str}')
])
buttons.append([InlineKeyboardButton("Отменить заказ", callback_data=f'admin_cancel|{uid}|{order_id_str}')])
buttons.append([InlineKeyboardButton("Удалить заказ", callback_data=f'admin_delete|{uid}|{order_id_str}')])
buttons.append([InlineKeyboardButton("👤 Открыть профиль", url=f"tg://user?id={uid}")])
buttons.append([InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')])
return InlineKeyboardMarkup(buttons)
</code_context>
<issue_to_address>
**предложение (качество_кода):** Объедините последовательные добавления в список в один extend [×3] ([`merge-list-appends-into-extend`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/merge-list-appends-into-extend/))
```suggestion
buttons.extend(
(
[
InlineKeyboardButton(
"Отменить заказ",
callback_data=f'admin_cancel|{uid}|{order_id_str}',
)
],
[
InlineKeyboardButton(
"Удалить заказ",
callback_data=f'admin_delete|{uid}|{order_id_str}',
)
],
[
InlineKeyboardButton(
"👤 Открыть профиль", url=f"tg://user?id={uid}"
)
],
[InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')],
)
)
```
</issue_to_address>
### Comment 11
<location> `bot.py:1164` </location>
<code_context>
async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
data = query.data
shared_routes = {
'back_to_main': main_menu,
'make_order': select_order_type,
'price_list': show_price_list,
'price_calculator': price_calculator,
'profile': show_profile,
'faq': show_faq,
}
if data in shared_routes:
context.user_data.pop('admin_state', None)
return await shared_routes[data](update, context)
if data == 'admin_menu':
return await show_admin_menu(update, context)
await answer_callback(query)
if data == 'admin_orders':
text_lines = []
buttons = []
updated = False
for uid, ords in ORDERS.items():
for ord_data in ords:
if ensure_order_payment_fields(ord_data):
updated = True
order_id = ord_data.get('order_id', 'N/A')
status = escape(ord_data.get('status', 'новый'))
payment_state = escape(ord_data.get('payment_state', 'не оплачен'))
user_href = escape(f"tg://user?id={uid}", quote=True)
text_lines.append(
f"#{order_id}: {status} | Оплата: {payment_state} — <a href=\"{user_href}\">{escape(str(uid))}</a>"
)
buttons.append([
InlineKeyboardButton(
f"#{order_id} ({uid})",
callback_data=f"admin_order|{uid}|{order_id}"
),
InlineKeyboardButton("👤", url=f"tg://user?id={uid}")
])
if updated:
save_json(ORDERS_FILE, ORDERS)
if not text_lines:
text = "Заказы отсутствуют."
else:
text = "📋 Заказы:\n" + "\n".join(text_lines[:20])
buttons.append([InlineKeyboardButton("Назад", callback_data='admin_menu')])
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(buttons), parse_mode=ParseMode.HTML)
return ADMIN_MENU
if data.startswith('admin_order|'):
try:
_, uid, order_id_str = data.split('|', 2)
except ValueError:
await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
order = next((o for o in ORDERS.get(uid, []) if str(o.get('order_id')) == order_id_str), None)
if not order:
await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
text = build_order_details(uid, order)
reply_markup = build_admin_order_keyboard(uid, order_id_str, order)
await query.edit_message_text(text, reply_markup=reply_markup, parse_mode=ParseMode.HTML)
return ADMIN_MENU
if data.startswith('admin_confirm_prepay|'):
try:
_, uid, order_id_str = data.split('|', 2)
except ValueError:
await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
order = next((o for o in ORDERS.get(uid, []) if str(o.get('order_id')) == order_id_str), None)
if not order:
await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
changed = ensure_order_payment_fields(order)
if order.get('prepayment_confirmed'):
if changed:
save_json(ORDERS_FILE, ORDERS)
info_prefix = "<b>ℹ️ Предоплата уже подтверждена.</b>"
else:
order['prepayment_confirmed'] = True
order['prepayment_confirmed_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if not order.get('full_payment_confirmed'):
order['payment_state'] = 'предоплата подтверждена'
credited = release_bonus(uid, order, 'prepayment')
save_json(ORDERS_FILE, ORDERS)
save_json(BONUSES_FILE, BONUSES)
info_prefix = "<b>✅ Предоплата подтверждена.</b>"
try:
balance = get_user_bonus_balance(uid)
credited_text = f"Начислено бонусов: {credited} ₽." if credited else "Бонусы будут начислены после полной оплаты."
message_text = (
f"Ваша предоплата по заказу #{order.get('order_id')} подтверждена. {credited_text}\n"
f"Текущий баланс бонусов: {balance} ₽."
)
await context.bot.send_message(int(uid), message_text)
except (TelegramError, ValueError) as exc:
logger.warning("Не удалось уведомить пользователя %s о предоплате: %s", uid, exc)
text = build_order_details(uid, order)
reply_markup = build_admin_order_keyboard(uid, order_id_str, order)
await query.edit_message_text(f"{info_prefix}<br><br>{text}", reply_markup=reply_markup, parse_mode=ParseMode.HTML)
return ADMIN_MENU
if data.startswith('admin_confirm_full|'):
try:
_, uid, order_id_str = data.split('|', 2)
except ValueError:
await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
order = next((o for o in ORDERS.get(uid, []) if str(o.get('order_id')) == order_id_str), None)
if not order:
await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
changed = ensure_order_payment_fields(order)
if order.get('full_payment_confirmed'):
if changed:
save_json(ORDERS_FILE, ORDERS)
info_prefix = "<b>ℹ️ Полная оплата уже подтверждена.</b>"
else:
order['full_payment_confirmed'] = True
order['full_payment_confirmed_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
order['payment_state'] = 'оплачен'
credited = release_bonus(uid, order, 'full')
save_json(ORDERS_FILE, ORDERS)
save_json(BONUSES_FILE, BONUSES)
info_prefix = "<b>✅ Оплата подтверждена.</b>"
try:
balance = get_user_bonus_balance(uid)
credited_text = f"Начислено бонусов: {credited} ₽." if credited else "Дополнительные бонусы не начислены."
message_text = (
f"Полная оплата по заказу #{order.get('order_id')} подтверждена. {credited_text}\n"
f"Текущий баланс бонусов: {balance} ₽."
)
await context.bot.send_message(int(uid), message_text)
except (TelegramError, ValueError) as exc:
logger.warning("Не удалось уведомить пользователя %s об оплате: %s", uid, exc)
text = build_order_details(uid, order)
reply_markup = build_admin_order_keyboard(uid, order_id_str, order)
await query.edit_message_text(f"{info_prefix}<br><br>{text}", reply_markup=reply_markup, parse_mode=ParseMode.HTML)
return ADMIN_MENU
if data.startswith('admin_cancel|'):
try:
_, uid, order_id_str = data.split('|', 2)
except ValueError:
await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
order = next((o for o in ORDERS.get(uid, []) if str(o.get('order_id')) == order_id_str), None)
if not order:
await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
order['status'] = 'отменен'
save_json(ORDERS_FILE, ORDERS)
text = f"Статус заказа #{order_id_str} обновлен на 'отменен'."
keyboard = [
[InlineKeyboardButton("Посмотреть заказ", callback_data=f'admin_order|{uid}|{order_id_str}')],
[InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')]
]
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
return ADMIN_MENU
if data.startswith('admin_delete|'):
try:
_, uid, order_id_str = data.split('|', 2)
except ValueError:
await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
orders_list = ORDERS.get(uid, [])
new_list = [o for o in orders_list if str(o.get('order_id')) != order_id_str]
if len(new_list) == len(orders_list):
await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
if new_list:
ORDERS[uid] = new_list
else:
ORDERS.pop(uid, None)
save_json(ORDERS_FILE, ORDERS)
text = f"Заказ #{order_id_str} удален."
keyboard = [
[InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')],
[InlineKeyboardButton("Админ-меню", callback_data='admin_menu')]
]
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
return ADMIN_MENU
text = ""
keyboard = [[InlineKeyboardButton("Назад", callback_data='admin_menu')]]
if data == 'admin_users':
text = "👥 Пользователи:\n" + "\n".join(f"ID: {uid}" for uid in ORDERS.keys())
elif data == 'admin_logs':
text = "📊 Логи (последние 10):\n"
for uid, logs in list(USER_LOGS.items())[-10:]:
if logs:
text += f"Пользователь {uid}: {logs[-1]['action']}\n"
elif data == 'admin_prices':
text = f"Текущий режим: {current_pricing_mode}\nВведите новый режим (hard/light):"
context.user_data['admin_state'] = 'change_mode'
elif data == 'admin_export':
df = pd.DataFrame([{'user_id': uid, **ord} for uid, ords in ORDERS.items() for ord in ords])
export_file = os.path.join(DATA_DIR, 'orders_export.csv')
df.to_csv(export_file, index=False)
await context.bot.send_document(ADMIN_CHAT_ID, open(export_file, 'rb'))
os.remove(export_file)
text = "📤 Экспорт отправлен!"
await query.edit_message_text(text or "Неизвестная команда. Возвращаюсь в админ-меню.", reply_markup=InlineKeyboardMarkup(keyboard))
return ADMIN_MENU
</code_context>
<issue_to_address>
**проблема (качество_кода):** Обнаружено низкое качество кода в `admin_menu_handler` - 4% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))
<br/><details><summary>Объяснение</summary>Показатель качества для этой функции ниже порогового значения качества в 25%.
Этот показатель представляет собой комбинацию длины метода, когнитивной сложности и рабочей памяти.
Как это можно исправить?
Возможно, стоит рефакторить эту функцию, чтобы сделать ее короче и читабельнее.
- Уменьшите длину функции, вынося части функциональности в отдельные функции. Это самое важное, что вы можете сделать — в идеале функция должна быть менее 10 строк.
- Уменьшите вложенность, возможно, путем введения защитных выражений для раннего возврата.
- Убедитесь, что переменные имеют узкую область видимости, чтобы код, использующий связанные концепции, располагался вместе внутри функции, а не был разбросан.</details>
</issue_to_address>Sourcery бесплатен для проектов с открытым исходным кодом — если вам нравятся наши обзоры, пожалуйста, рассмотрите возможность поделиться ими ✨
Original comment in English
Hey there - I've reviewed your changes and they look great!
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `bot.py:656-667` </location>
<code_context>
+ return await prompt_contact_input(update, context, via_callback=True)
+ return await prompt_contact_input(update, context)
async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
contact = update.message.text.strip()
+ if contact == BACK_BUTTON_TEXT:
+ await update.message.reply_text("Возвращаемся к файлам.", reply_markup=ReplyKeyboardRemove())
+ return await prompt_file_upload(update, context, from_back=True)
if not contact:
await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
return INPUT_CONTACT
context.user_data['current_contact'] = contact
context.user_data['last_contact'] = contact
+ await update.message.reply_text("Контакт сохранен.", reply_markup=ReplyKeyboardRemove())
return await add_upsell(update, context)
+async def back_from_topic(update: Update, context: ContextTypes.DEFAULT_TYPE):
</code_context>
<issue_to_address>
**suggestion:** Contact validation could be stricter to avoid invalid entries.
Consider implementing validation to ensure contacts match expected formats, such as Telegram handles, email addresses, or VK links.
```suggestion
import re
def is_valid_contact(contact: str) -> bool:
# Telegram handle: starts with @, 5-32 chars, letters, numbers, underscores
telegram_pattern = r"^@[A-Za-z0-9_]{5,32}$"
# Email address
email_pattern = r"^[\w\.-]+@[\w\.-]+\.\w{2,}$"
# VK link: vk.com/id123456 or vk.com/username
vk_pattern = r"^(https?://)?(www\.)?vk\.com/(id\d+|[A-Za-z0-9_\.]+)$"
return (
re.match(telegram_pattern, contact)
or re.match(email_pattern, contact)
or re.match(vk_pattern, contact)
)
async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
contact = update.message.text.strip()
if contact == BACK_BUTTON_TEXT:
await update.message.reply_text("Возвращаемся к файлам.", reply_markup=ReplyKeyboardRemove())
return await prompt_file_upload(update, context, from_back=True)
if not contact:
await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
return INPUT_CONTACT
if not is_valid_contact(contact):
await update.message.reply_text(
"Пожалуйста, укажите корректный контакт: Telegram (@username), email, или ссылку на VK.",
reply_markup=ReplyKeyboardRemove()
)
return INPUT_CONTACT
context.user_data['current_contact'] = contact
context.user_data['last_contact'] = contact
await update.message.reply_text("Контакт сохранен.", reply_markup=ReplyKeyboardRemove())
return await add_upsell(update, context)
```
</issue_to_address>
### Comment 2
<location> `bot.py:1220-1229` </location>
<code_context>
+ return InlineKeyboardMarkup(buttons)
+
# Обработчик админ-меню
async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
</code_context>
<issue_to_address>
**issue (bug_risk):** Admin payment confirmation handlers lack concurrency protection for file writes.
Simultaneous admin actions may cause data corruption due to concurrent writes. Implement file locking or atomic operations to ensure safe access.
</issue_to_address>
### Comment 3
<location> `bot.py:488-498` </location>
<code_context>
+ keyboard = [[InlineKeyboardButton(f"{val['icon']} {val['name']}", callback_data=f'type_{key}')] for key, val in ORDER_TYPES.items()]
+ keyboard.append([InlineKeyboardButton("⬅️ Меню", callback_data='back_to_main')])
+ reply_markup = InlineKeyboardMarkup(keyboard)
+ if query:
+ try:
+ await query.edit_message_text(text, reply_markup=reply_markup)
+ except TelegramError as e:
+ if "message is not modified" in str(e).lower():
+ pass
+ else:
+ raise
+ else:
+ await update.message.reply_text(text, reply_markup=reply_markup)
+ return SELECT_ORDER_TYPE
# Подробности о типе заказа
</code_context>
<issue_to_address>
**suggestion:** select_order_type handler should handle unexpected update types.
Consider adding error handling for cases where neither a callback query nor a message is present to prevent unexpected failures.
```suggestion
if query:
try:
await query.edit_message_text(text, reply_markup=reply_markup)
except TelegramError as e:
if "message is not modified" in str(e).lower():
pass
else:
raise
elif getattr(update, "message", None):
await update.message.reply_text(text, reply_markup=reply_markup)
else:
# Handle unexpected update type
logger.error("select_order_type: Neither callback_query nor message present in update: %s", update)
return ConversationHandler.END
return SELECT_ORDER_TYPE
```
</issue_to_address>
### Comment 4
<location> `bot.py:119` </location>
<code_context>
def release_bonus(user_id: str, order: dict, stage: str) -> int:
ensure_order_payment_fields(order)
user_key = str(user_id)
bonus_entry = BONUSES.setdefault(user_key, {'balance': 0, 'history': []})
amount = 0
if stage == 'prepayment':
if order.get('bonus_released_prepaid'):
return 0
amount = order.get('bonus_total', 0) // 2
order['bonus_released_prepaid'] = amount
elif stage == 'full':
if order.get('bonus_released_full'):
return 0
already = order.get('bonus_released_prepaid', 0)
amount = max(order.get('bonus_total', 0) - already, 0)
order['bonus_released_full'] = amount
if amount <= 0:
return 0
bonus_entry['balance'] = bonus_entry.get('balance', 0) + amount
history = bonus_entry.setdefault('history', [])
history.append({
'order_id': order.get('order_id'),
'amount': amount,
'stage': stage,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
return amount
</code_context>
<issue_to_address>
**issue (code-quality):** We've found these issues:
- Remove unnecessary casts to int, str, float or bool ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))
- Simplify conditional into switch-like form [×2] ([`switch`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/switch/))
</issue_to_address>
### Comment 5
<location> `bot.py:146` </location>
<code_context>
def get_user_bonus_balance(user_id: str) -> int:
entry = BONUSES.get(str(user_id), {})
return int(entry.get('balance', 0))
</code_context>
<issue_to_address>
**suggestion (code-quality):** Remove unnecessary casts to int, str, float or bool ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))
```suggestion
entry = BONUSES.get(user_id, {})
```
</issue_to_address>
### Comment 6
<location> `bot.py:152` </location>
<code_context>
def get_pending_bonus(user_id: str) -> int:
total = 0
changed = False
for order in ORDERS.get(str(user_id), []):
if ensure_order_payment_fields(order):
changed = True
credited = order.get('bonus_released_prepaid', 0) + order.get('bonus_released_full', 0)
total += max(order.get('bonus_total', 0) - credited, 0)
if changed:
save_json(ORDERS_FILE, ORDERS)
return int(total)
</code_context>
<issue_to_address>
**suggestion (code-quality):** Remove unnecessary casts to int, str, float or bool ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))
```suggestion
for order in ORDERS.get(user_id, []):
```
</issue_to_address>
### Comment 7
<location> `bot.py:192` </location>
<code_context>
def build_order_details(uid: str, order: dict) -> str:
changed = ensure_order_payment_fields(order)
if changed:
save_json(ORDERS_FILE, ORDERS)
order_id = order.get('order_id', 'N/A')
order_name = ORDER_TYPES.get(order.get('type'), {}).get('name', order.get('type', 'Неизвестно'))
user_link = escape(f"tg://user?id={uid}", quote=True)
lines = [
f"<b>Заказ #{order_id}</b>",
f"Пользователь: <a href=\"{user_link}\">{escape(str(uid))}</a>",
f"Тип: {escape(order_name)}",
f"Тема: {escape(order.get('topic', 'Без темы'))}",
f"Срок: {order.get('deadline_days', 'N/A')} дней",
f"Статус: {escape(order.get('status', 'неизвестно'))}",
f"Оплата: {escape(order.get('payment_state', 'не оплачен'))}",
f"Контакт: {format_contact_link(order.get('contact'))}",
f"Требования: {escape(order.get('requirements', 'Нет'))}",
]
if order.get('upsells'):
upsells_readable = ', '.join(UPSELL_TITLES.get(code, code) for code in order['upsells'])
lines.append(f"Допы: {escape(upsells_readable)}")
else:
lines.append("Допы: нет")
lines.append(f"Файлов: {len(order.get('attachments') or [])}")
bonus_total = order.get('bonus_total', 0)
if bonus_total:
lines.append(
f"Бонусы: всего {bonus_total} ₽ | предоплата {order.get('bonus_released_prepaid', 0)} ₽ | оплата {order.get('bonus_released_full', 0)} ₽"
)
if order.get('prepayment_confirmed_at'):
lines.append(f"Предоплата подтверждена: {escape(order['prepayment_confirmed_at'])}")
if order.get('full_payment_confirmed_at'):
lines.append(f"Оплата подтверждена: {escape(order['full_payment_confirmed_at'])}")
if order.get('created_at'):
lines.append(f"Создан: {escape(order['created_at'])}")
return '<br>'.join(lines)
</code_context>
<issue_to_address>
**issue (code-quality):** We've found these issues:
- Use named expression to simplify assignment and conditional [×2] ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
- Remove unnecessary casts to int, str, float or bool ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))
</issue_to_address>
### Comment 8
<location> `bot.py:492-494` </location>
<code_context>
async def select_order_type(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await answer_callback(query)
data = query.data if query else None
user = update.effective_user
log_user_action(user.id, user.username, "Выбор типа заказа")
if data == 'back_to_main':
return await main_menu(update, context)
text = "Выберите тип работы (добавьте несколько в корзину для скидки!):"
keyboard = [[InlineKeyboardButton(f"{val['icon']} {val['name']}", callback_data=f'type_{key}')] for key, val in ORDER_TYPES.items()]
keyboard.append([InlineKeyboardButton("⬅️ Меню", callback_data='back_to_main')])
reply_markup = InlineKeyboardMarkup(keyboard)
if query:
try:
await query.edit_message_text(text, reply_markup=reply_markup)
except TelegramError as e:
if "message is not modified" in str(e).lower():
pass
else:
raise
else:
await update.message.reply_text(text, reply_markup=reply_markup)
return SELECT_ORDER_TYPE
</code_context>
<issue_to_address>
**suggestion (code-quality):** Swap if/else to remove empty if body ([`remove-pass-body`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-pass-body/))
```suggestion
if "message is not modified" not in str(e).lower():
```
</issue_to_address>
### Comment 9
<location> `bot.py:1070` </location>
<code_context>
async def show_orders(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await answer_callback(query)
data = query.data
if data == 'profile':
return await show_profile(update, context)
user_id = str(update.effective_user.id)
user_orders = ORDERS.get(user_id, [])
if not user_orders:
text = "Пока нет заказов. Сделайте заказ сейчас!"
else:
text = "Ваши заказы:\n"
changed = False
for order in user_orders:
name = ORDER_TYPES.get(order.get('type'), {}).get('name', 'Неизвестно')
if ensure_order_payment_fields(order):
changed = True
payment_state = order.get('payment_state', 'не оплачен')
text += f"#{order.get('order_id', 'N/A')}: {name} - {order.get('status', 'новый')} | Оплата: {payment_state}\n"
if changed:
save_json(ORDERS_FILE, ORDERS)
keyboard = [[InlineKeyboardButton("Назад", callback_data='profile')]]
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
return SHOW_ORDERS
</code_context>
<issue_to_address>
**issue (code-quality):** We've found these issues:
- Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
- Swap if/else branches ([`swap-if-else-branches`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/swap-if-else-branches/))
</issue_to_address>
### Comment 10
<location> `bot.py:1157-1160` </location>
<code_context>
def build_admin_order_keyboard(uid: str, order_id_str: str, order: dict) -> InlineKeyboardMarkup:
buttons = []
if not order.get('prepayment_confirmed'):
buttons.append([
InlineKeyboardButton("Подтвердить предоплату", callback_data=f'admin_confirm_prepay|{uid}|{order_id_str}')
])
if not order.get('full_payment_confirmed'):
buttons.append([
InlineKeyboardButton("Подтвердить оплату", callback_data=f'admin_confirm_full|{uid}|{order_id_str}')
])
buttons.append([InlineKeyboardButton("Отменить заказ", callback_data=f'admin_cancel|{uid}|{order_id_str}')])
buttons.append([InlineKeyboardButton("Удалить заказ", callback_data=f'admin_delete|{uid}|{order_id_str}')])
buttons.append([InlineKeyboardButton("👤 Открыть профиль", url=f"tg://user?id={uid}")])
buttons.append([InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')])
return InlineKeyboardMarkup(buttons)
</code_context>
<issue_to_address>
**suggestion (code-quality):** Merge consecutive list appends into a single extend [×3] ([`merge-list-appends-into-extend`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/merge-list-appends-into-extend/))
```suggestion
buttons.extend(
(
[
InlineKeyboardButton(
"Отменить заказ",
callback_data=f'admin_cancel|{uid}|{order_id_str}',
)
],
[
InlineKeyboardButton(
"Удалить заказ",
callback_data=f'admin_delete|{uid}|{order_id_str}',
)
],
[
InlineKeyboardButton(
"👤 Открыть профиль", url=f"tg://user?id={uid}"
)
],
[InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')],
)
)
```
</issue_to_address>
### Comment 11
<location> `bot.py:1164` </location>
<code_context>
async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
data = query.data
shared_routes = {
'back_to_main': main_menu,
'make_order': select_order_type,
'price_list': show_price_list,
'price_calculator': price_calculator,
'profile': show_profile,
'faq': show_faq,
}
if data in shared_routes:
context.user_data.pop('admin_state', None)
return await shared_routes[data](update, context)
if data == 'admin_menu':
return await show_admin_menu(update, context)
await answer_callback(query)
if data == 'admin_orders':
text_lines = []
buttons = []
updated = False
for uid, ords in ORDERS.items():
for ord_data in ords:
if ensure_order_payment_fields(ord_data):
updated = True
order_id = ord_data.get('order_id', 'N/A')
status = escape(ord_data.get('status', 'новый'))
payment_state = escape(ord_data.get('payment_state', 'не оплачен'))
user_href = escape(f"tg://user?id={uid}", quote=True)
text_lines.append(
f"#{order_id}: {status} | Оплата: {payment_state} — <a href=\"{user_href}\">{escape(str(uid))}</a>"
)
buttons.append([
InlineKeyboardButton(
f"#{order_id} ({uid})",
callback_data=f"admin_order|{uid}|{order_id}"
),
InlineKeyboardButton("👤", url=f"tg://user?id={uid}")
])
if updated:
save_json(ORDERS_FILE, ORDERS)
if not text_lines:
text = "Заказы отсутствуют."
else:
text = "📋 Заказы:\n" + "\n".join(text_lines[:20])
buttons.append([InlineKeyboardButton("Назад", callback_data='admin_menu')])
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(buttons), parse_mode=ParseMode.HTML)
return ADMIN_MENU
if data.startswith('admin_order|'):
try:
_, uid, order_id_str = data.split('|', 2)
except ValueError:
await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
order = next((o for o in ORDERS.get(uid, []) if str(o.get('order_id')) == order_id_str), None)
if not order:
await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
text = build_order_details(uid, order)
reply_markup = build_admin_order_keyboard(uid, order_id_str, order)
await query.edit_message_text(text, reply_markup=reply_markup, parse_mode=ParseMode.HTML)
return ADMIN_MENU
if data.startswith('admin_confirm_prepay|'):
try:
_, uid, order_id_str = data.split('|', 2)
except ValueError:
await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
order = next((o for o in ORDERS.get(uid, []) if str(o.get('order_id')) == order_id_str), None)
if not order:
await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
changed = ensure_order_payment_fields(order)
if order.get('prepayment_confirmed'):
if changed:
save_json(ORDERS_FILE, ORDERS)
info_prefix = "<b>ℹ️ Предоплата уже подтверждена.</b>"
else:
order['prepayment_confirmed'] = True
order['prepayment_confirmed_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if not order.get('full_payment_confirmed'):
order['payment_state'] = 'предоплата подтверждена'
credited = release_bonus(uid, order, 'prepayment')
save_json(ORDERS_FILE, ORDERS)
save_json(BONUSES_FILE, BONUSES)
info_prefix = "<b>✅ Предоплата подтверждена.</b>"
try:
balance = get_user_bonus_balance(uid)
credited_text = f"Начислено бонусов: {credited} ₽." if credited else "Бонусы будут начислены после полной оплаты."
message_text = (
f"Ваша предоплата по заказу #{order.get('order_id')} подтверждена. {credited_text}\n"
f"Текущий баланс бонусов: {balance} ₽."
)
await context.bot.send_message(int(uid), message_text)
except (TelegramError, ValueError) as exc:
logger.warning("Не удалось уведомить пользователя %s о предоплате: %s", uid, exc)
text = build_order_details(uid, order)
reply_markup = build_admin_order_keyboard(uid, order_id_str, order)
await query.edit_message_text(f"{info_prefix}<br><br>{text}", reply_markup=reply_markup, parse_mode=ParseMode.HTML)
return ADMIN_MENU
if data.startswith('admin_confirm_full|'):
try:
_, uid, order_id_str = data.split('|', 2)
except ValueError:
await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
order = next((o for o in ORDERS.get(uid, []) if str(o.get('order_id')) == order_id_str), None)
if not order:
await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
changed = ensure_order_payment_fields(order)
if order.get('full_payment_confirmed'):
if changed:
save_json(ORDERS_FILE, ORDERS)
info_prefix = "<b>ℹ️ Полная оплата уже подтверждена.</b>"
else:
order['full_payment_confirmed'] = True
order['full_payment_confirmed_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
order['payment_state'] = 'оплачен'
credited = release_bonus(uid, order, 'full')
save_json(ORDERS_FILE, ORDERS)
save_json(BONUSES_FILE, BONUSES)
info_prefix = "<b>✅ Оплата подтверждена.</b>"
try:
balance = get_user_bonus_balance(uid)
credited_text = f"Начислено бонусов: {credited} ₽." if credited else "Дополнительные бонусы не начислены."
message_text = (
f"Полная оплата по заказу #{order.get('order_id')} подтверждена. {credited_text}\n"
f"Текущий баланс бонусов: {balance} ₽."
)
await context.bot.send_message(int(uid), message_text)
except (TelegramError, ValueError) as exc:
logger.warning("Не удалось уведомить пользователя %s об оплате: %s", uid, exc)
text = build_order_details(uid, order)
reply_markup = build_admin_order_keyboard(uid, order_id_str, order)
await query.edit_message_text(f"{info_prefix}<br><br>{text}", reply_markup=reply_markup, parse_mode=ParseMode.HTML)
return ADMIN_MENU
if data.startswith('admin_cancel|'):
try:
_, uid, order_id_str = data.split('|', 2)
except ValueError:
await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
order = next((o for o in ORDERS.get(uid, []) if str(o.get('order_id')) == order_id_str), None)
if not order:
await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
order['status'] = 'отменен'
save_json(ORDERS_FILE, ORDERS)
text = f"Статус заказа #{order_id_str} обновлен на 'отменен'."
keyboard = [
[InlineKeyboardButton("Посмотреть заказ", callback_data=f'admin_order|{uid}|{order_id_str}')],
[InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')]
]
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
return ADMIN_MENU
if data.startswith('admin_delete|'):
try:
_, uid, order_id_str = data.split('|', 2)
except ValueError:
await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
orders_list = ORDERS.get(uid, [])
new_list = [o for o in orders_list if str(o.get('order_id')) != order_id_str]
if len(new_list) == len(orders_list):
await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]]))
return ADMIN_MENU
if new_list:
ORDERS[uid] = new_list
else:
ORDERS.pop(uid, None)
save_json(ORDERS_FILE, ORDERS)
text = f"Заказ #{order_id_str} удален."
keyboard = [
[InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')],
[InlineKeyboardButton("Админ-меню", callback_data='admin_menu')]
]
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
return ADMIN_MENU
text = ""
keyboard = [[InlineKeyboardButton("Назад", callback_data='admin_menu')]]
if data == 'admin_users':
text = "👥 Пользователи:\n" + "\n".join(f"ID: {uid}" for uid in ORDERS.keys())
elif data == 'admin_logs':
text = "📊 Логи (последние 10):\n"
for uid, logs in list(USER_LOGS.items())[-10:]:
if logs:
text += f"Пользователь {uid}: {logs[-1]['action']}\n"
elif data == 'admin_prices':
text = f"Текущий режим: {current_pricing_mode}\nВведите новый режим (hard/light):"
context.user_data['admin_state'] = 'change_mode'
elif data == 'admin_export':
df = pd.DataFrame([{'user_id': uid, **ord} for uid, ords in ORDERS.items() for ord in ords])
export_file = os.path.join(DATA_DIR, 'orders_export.csv')
df.to_csv(export_file, index=False)
await context.bot.send_document(ADMIN_CHAT_ID, open(export_file, 'rb'))
os.remove(export_file)
text = "📤 Экспорт отправлен!"
await query.edit_message_text(text or "Неизвестная команда. Возвращаюсь в админ-меню.", reply_markup=InlineKeyboardMarkup(keyboard))
return ADMIN_MENU
</code_context>
<issue_to_address>
**issue (code-quality):** Low code quality found in admin\_menu\_handler - 4% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))
<br/><details><summary>Explanation</summary>The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.
How can you solve this?
It might be worth refactoring this function to make it shorter and more readable.
- Reduce the function length by extracting pieces of functionality out into
their own functions. This is the most important thing you can do - ideally a
function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
sits together within the function rather than being scattered.</details>
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if query: | ||
| try: | ||
| await query.edit_message_text(text, reply_markup=reply_markup) | ||
| except TelegramError as e: | ||
| if "message is not modified" in str(e).lower(): | ||
| pass | ||
| else: | ||
| raise | ||
| else: | ||
| await update.message.reply_text(text, reply_markup=reply_markup) | ||
| return SELECT_ORDER_TYPE |
There was a problem hiding this comment.
предложение: Обработчик select_order_type должен обрабатывать неожиданные типы обновлений.
Рассмотрите возможность добавления обработки ошибок для случаев, когда отсутствует как callback query, так и message, чтобы предотвратить неожиданные сбои.
| if query: | |
| try: | |
| await query.edit_message_text(text, reply_markup=reply_markup) | |
| except TelegramError as e: | |
| if "message is not modified" in str(e).lower(): | |
| pass | |
| else: | |
| raise | |
| else: | |
| await update.message.reply_text(text, reply_markup=reply_markup) | |
| return SELECT_ORDER_TYPE | |
| if query: | |
| try: | |
| await query.edit_message_text(text, reply_markup=reply_markup) | |
| except TelegramError as e: | |
| if "message is not modified" in str(e).lower(): | |
| pass | |
| else: | |
| raise | |
| elif getattr(update, "message", None): | |
| await update.message.reply_text(text, reply_markup=reply_markup) | |
| else: | |
| # Handle unexpected update type | |
| logger.error("select_order_type: Neither callback_query nor message present in update: %s", update) | |
| return ConversationHandler.END | |
| return SELECT_ORDER_TYPE |
Original comment in English
suggestion: select_order_type handler should handle unexpected update types.
Consider adding error handling for cases where neither a callback query nor a message is present to prevent unexpected failures.
| if query: | |
| try: | |
| await query.edit_message_text(text, reply_markup=reply_markup) | |
| except TelegramError as e: | |
| if "message is not modified" in str(e).lower(): | |
| pass | |
| else: | |
| raise | |
| else: | |
| await update.message.reply_text(text, reply_markup=reply_markup) | |
| return SELECT_ORDER_TYPE | |
| if query: | |
| try: | |
| await query.edit_message_text(text, reply_markup=reply_markup) | |
| except TelegramError as e: | |
| if "message is not modified" in str(e).lower(): | |
| pass | |
| else: | |
| raise | |
| elif getattr(update, "message", None): | |
| await update.message.reply_text(text, reply_markup=reply_markup) | |
| else: | |
| # Handle unexpected update type | |
| logger.error("select_order_type: Neither callback_query nor message present in update: %s", update) | |
| return ConversationHandler.END | |
| return SELECT_ORDER_TYPE |
| if "message is not modified" in str(e).lower(): | ||
| pass | ||
| else: |
There was a problem hiding this comment.
предложение (качество_кода): Поменяйте местами if/else, чтобы убрать пустой блок if (remove-pass-body)
| if "message is not modified" in str(e).lower(): | |
| pass | |
| else: | |
| if "message is not modified" not in str(e).lower(): |
Original comment in English
suggestion (code-quality): Swap if/else to remove empty if body (remove-pass-body)
| if "message is not modified" in str(e).lower(): | |
| pass | |
| else: | |
| if "message is not modified" not in str(e).lower(): |
Summary
Testing
https://chatgpt.com/codex/tasks/task_e_68ca3725e308832f8482a8783e3e1c64
Сводка от Sourcery
Реализовать процесс подтверждения платежей с начислением бонусов, добавить отслеживание бонусов пользователей, улучшить навигацию при создании заказа и отобразить информацию о платежах и бонусах.
Новые возможности:
Улучшения:
Original summary in English
Summary by Sourcery
Implement payment confirmation flow with bonus release, add user bonus tracking, improve order creation navigation, and surface payment and bonus info
New Features:
Enhancements: