Skip to content

Require contact info and manage attachments in order flow - #2

Open
soloveyska1 wants to merge 4 commits into
mainfrom
codex/update-order-process-and-notifications
Open

Require contact info and manage attachments in order flow#2
soloveyska1 wants to merge 4 commits into
mainfrom
codex/update-order-process-and-notifications

Conversation

@soloveyska1

@soloveyska1 soloveyska1 commented Sep 16, 2025

Copy link
Copy Markdown
Owner

Summary

  • clarify the welcome message and theoretical course description to reflect the updated focus
  • require clients to provide contact details, support optional file uploads, and forward detailed order summaries with attachments to the administrator
  • extend the admin panel with order detail screens plus actions to cancel or delete orders

Testing

  • python -m compileall bot.py

https://chatgpt.com/codex/tasks/task_e_68c98bc7f7b0832faa80ff9d9e92bc1e

Сводка от Sourcery

Обновить процесс оформления заказа, чтобы требовать контактную информацию клиента, добавить поддержку вложений файлов и улучшить возможности администратора по управлению заказами.

Новые функции:

  • Требовать от клиентов предоставления контактной информации при оформлении заказов.
  • Позволить клиентам загружать и прикреплять файлы к своим заказам.
  • Пересылать подробные сводки заказов, включая вложения, администратору.
  • Добавить экраны в админ-панели для просмотра деталей заказа и выполнения действий с заказом, таких как отмена или удаление.

Улучшения:

  • Уточнить приветственное сообщение и описание теоретического курса, чтобы они соответствовали обновленному фокусу услуг.
Original summary in English

Summary by Sourcery

Update the order flow to require client contact information, support file attachments, and enhance admin order management capabilities.

New Features:

  • Require clients to provide contact information when placing orders.
  • Allow clients to upload and attach files to their orders.
  • Forward detailed order summaries, including attachments, to the administrator.
  • Add admin panel screens for viewing order details and performing order actions such as cancel or delete.

Enhancements:

  • Clarify welcome message and theoretical course description to reflect updated service focus.

@sourcery-ai

sourcery-ai Bot commented Sep 16, 2025

Copy link
Copy Markdown

Руководство для рецензента

Этот PR обогащает процесс оформления заказа, добавляя необязательную загрузку файлов и обязательный шаг ввода контактных данных, обновляя модель данных заказа, улучшая уведомления как для пользователей, так и для администраторов с помощью форматированного HTML-вывода и отправки вложений, а также обновляя панель администратора для навигации, просмотра, отмены или удаления отдельных заказов, а также содержит мелкие доработки пользовательского интерфейса и констант.

Диаграмма последовательности для оформления заказа с контактными данными и вложениями

sequenceDiagram
    actor User
    participant Bot
    participant Admin
    User->>Bot: Start order
    Bot->>User: Ask for order type
    User->>Bot: Select type
    Bot->>User: Ask for topic
    User->>Bot: Enter topic
    Bot->>User: Ask for deadline
    User->>Bot: Select deadline
    Bot->>User: Ask for requirements
    User->>Bot: Enter requirements or skip
    Bot->>User: Prompt for file uploads
    User->>Bot: Upload files (optional)
    User->>Bot: /done or /skip
    Bot->>User: Request contact info (required)
    User->>Bot: Enter contact info
    Bot->>User: Ask for upsells
    User->>Bot: Select upsells (optional)
    Bot->>User: Confirm cart
    User->>Bot: Confirm order
    Bot->>Admin: Send order summary with contact and attachments
    Bot->>User: Send confirmation
Loading

Диаграмма последовательности для действий по управлению заказами в панели администратора

sequenceDiagram
    actor Admin
    participant Bot
    Admin->>Bot: Open admin panel
    Bot->>Admin: Show list of orders
    Admin->>Bot: Select order
    Bot->>Admin: Show order details (with contact and attachments info)
    Admin->>Bot: Cancel or delete order
    Bot->>Admin: Update order status or remove order
    Bot->>Admin: Confirm action
Loading

Диаграмма классов для обновленной структуры данных заказа

classDiagram
    class Order {
        +type: str
        +topic: str
        +deadline_days: int
        +requirements: str
        +upsells: list
        +price: int
        +status: str
        +attachments: list
        +contact: str
        +created_at: str
        +order_id: int
    }
    class Attachment {
        +type: str
        +file_id: str
        +file_name: str
        +mime_type: str
        +file_unique_id: str
        +caption: str
    }
    Order "1" -- "*" Attachment: contains
Loading

Изменения на уровне файлов

Изменение Подробности Files
Введение сбора вложений и контактных данных в диалоговом потоке
  • Добавление состояний диалога UPLOAD_FILES и INPUT_CONTACT
  • Реализация prompt_file_upload, handle_document_upload, handle_photo_upload, files_text_reminder, skip_files, finish_files
  • Добавление request_contact и input_contact для принудительного ввода обязательных контактных данных
bot.py
Расширение создания заказа для включения вложений, контактных данных и временной метки
  • Изменение process_order для получения вложений и контактных данных из user_data
  • Добавление полей 'attachments', 'contact' и 'created_at' в словарь заказа
  • Очистка временных ключей (current_files, current_contact) после добавления в корзину
bot.py
Улучшение подтверждения корзины и уведомлений администратора с помощью HTML и вложений
  • Обновление confirm_cart для отображения количества вложений и контактных данных для каждого заказа
  • Доработка confirm_cart_handler для создания подробного HTML-резюме с помощью format_contact_link
  • Отправка каждого вложения в ADMIN_CHAT_ID с соответствующими подписями
bot.py
Обновление панели администратора для детального управления заказами
  • Рефакторинг admin_menu_handler для отображения списка заказов с встроенными кнопками навигации
  • Реализация build_order_details для просмотра заказа в HTML-формате
  • Добавление коллбэков admin_cancel и admin_delete для обновления или удаления заказов и сохранения с помощью save_json
bot.py
Добавление утилит и доработка текстов пользовательского интерфейса
  • Введение format_contact_link для преобразования контактных строк в кликабельные ссылки
  • Добавление сопоставления UPSELL_TITLES для отображения допродаж
  • Обновление приветственного сообщения и текста описания теоретического курса
bot.py

Советы и команды

Взаимодействие с Sourcery

  • Запустить новую проверку: Прокомментируйте @sourcery-ai review в запросе на вытягивание.
  • Продолжить обсуждения: Отвечайте непосредственно на комментарии Sourcery.
  • Сгенерировать проблему GitHub из комментария к проверке: Попросите Sourcery создать проблему из комментария к проверке, ответив на него. Вы также можете ответить на комментарий к проверке с помощью @sourcery-ai issue, чтобы создать из него проблему.
  • Сгенерировать заголовок запроса на вытягивание: Напишите @sourcery-ai в любом месте заголовка запроса на вытягивание, чтобы сгенерировать заголовок в любое время. Вы также можете прокомментировать @sourcery-ai title в запросе на вытягивание, чтобы (пере)генерировать заголовок в любое время.
  • Сгенерировать сводку запроса на вытягивание: Напишите @sourcery-ai summary в любом месте тела запроса на вытягивание, чтобы сгенерировать сводку PR в любое время именно там, где вы хотите. Вы также можете прокомментировать @sourcery-ai summary в запросе на вытягивание, чтобы (пере)генерировать сводку в любое время.
  • Сгенерировать руководство для рецензента: Прокомментируйте @sourcery-ai guide в запросе на вытягивание, чтобы (пере)генерировать руководство для рецензента в любое время.
  • Разрешить все комментарии Sourcery: Прокомментируйте @sourcery-ai resolve в запросе на вытягивание, чтобы разрешить все комментарии Sourcery. Полезно, если вы уже рассмотрели все комментарии и больше не хотите их видеть.
  • Отклонить все проверки Sourcery: Прокомментируйте @sourcery-ai dismiss в запросе на вытягивание, чтобы отклонить все существующие проверки Sourcery. Особенно полезно, если вы хотите начать новую проверку с чистого листа — не забудьте прокомментировать @sourcery-ai review, чтобы запустить новую проверку!

Настройка вашего опыта

Получите доступ к своей панели управления, чтобы:

  • Включить или отключить функции проверки, такие как сгенерированная Sourcery сводка запроса на вытягивание, руководство для рецензента и другие.
  • Изменить язык проверки.
  • Добавить, удалить или отредактировать пользовательские инструкции по проверке.
  • Настроить другие параметры проверки.

Получение помощи

Original review guide in English

Reviewer's Guide

This PR enriches the order flow by inserting optional file uploads and a required contact step, updating the order data model, enhancing both user and admin notifications with rich HTML output and attachment dispatch, and upgrading the admin panel to navigate, view, cancel, or delete individual orders, plus minor UI and constant tweaks.

Sequence diagram for order placement with contact and attachments

sequenceDiagram
    actor User
    participant Bot
    participant Admin
    User->>Bot: Start order
    Bot->>User: Ask for order type
    User->>Bot: Select type
    Bot->>User: Ask for topic
    User->>Bot: Enter topic
    Bot->>User: Ask for deadline
    User->>Bot: Select deadline
    Bot->>User: Ask for requirements
    User->>Bot: Enter requirements or skip
    Bot->>User: Prompt for file uploads
    User->>Bot: Upload files (optional)
    User->>Bot: /done or /skip
    Bot->>User: Request contact info (required)
    User->>Bot: Enter contact info
    Bot->>User: Ask for upsells
    User->>Bot: Select upsells (optional)
    Bot->>User: Confirm cart
    User->>Bot: Confirm order
    Bot->>Admin: Send order summary with contact and attachments
    Bot->>User: Send confirmation
Loading

Sequence diagram for admin panel order management actions

sequenceDiagram
    actor Admin
    participant Bot
    Admin->>Bot: Open admin panel
    Bot->>Admin: Show list of orders
    Admin->>Bot: Select order
    Bot->>Admin: Show order details (with contact and attachments info)
    Admin->>Bot: Cancel or delete order
    Bot->>Admin: Update order status or remove order
    Bot->>Admin: Confirm action
Loading

Class diagram for updated order data structure

classDiagram
    class Order {
        +type: str
        +topic: str
        +deadline_days: int
        +requirements: str
        +upsells: list
        +price: int
        +status: str
        +attachments: list
        +contact: str
        +created_at: str
        +order_id: int
    }
    class Attachment {
        +type: str
        +file_id: str
        +file_name: str
        +mime_type: str
        +file_unique_id: str
        +caption: str
    }
    Order "1" -- "*" Attachment: contains
Loading

File-Level Changes

Change Details Files
Introduce attachment and contact collection in the conversation flow
  • Add UPLOAD_FILES and INPUT_CONTACT conversation states
  • Implement prompt_file_upload, handle_document_upload, handle_photo_upload, files_text_reminder, skip_files, finish_files
  • Add request_contact and input_contact to enforce mandatory contact input
bot.py
Extend order creation to include attachments, contact, and timestamp
  • Modify process_order to capture attachments and contact from user_data
  • Add 'attachments', 'contact', and 'created_at' fields to the order dict
  • Clear temporary keys (current_files, current_contact) after appending to cart
bot.py
Enhance cart confirmation and admin notifications with HTML and attachments
  • Update confirm_cart to display attachment counts and contact per order
  • Refine confirm_cart_handler to build a detailed HTML summary via format_contact_link
  • Send each attachment to ADMIN_CHAT_ID with proper captions
bot.py
Upgrade admin panel for detailed order management
  • Refactor admin_menu_handler to list orders with inline navigation buttons
  • Implement build_order_details for HTML-formatted order view
  • Add callbacks admin_cancel and admin_delete to update or remove orders and persist via save_json
bot.py
Add utilities and tweak UI copy
  • Introduce format_contact_link to render contact strings as clickable links
  • Add UPSELL_TITLES mapping for upsell display
  • Update welcome message and theoretical course description text
bot.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Привет! Я просмотрел твои изменения — вот несколько замечаний:

  • Рассмотрите возможность рефакторинга логики загрузки файлов и сбора контактов в выделенные модули или классы, чтобы уменьшить размер bot.py и улучшить удобство сопровождения.
  • Добавьте проверку для загружаемых вложений (например, размер файла, тип MIME), чтобы предотвратить отправку пользователями слишком больших или неподдерживаемых файлов.
  • Список заказов администратора в настоящее время отображает все элементы сразу; рассмотрите возможность реализации пагинации или динамической загрузки для более эффективного управления большим количеством заказов.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider refactoring the file upload and contact collection logic into dedicated modules or classes to reduce the size of bot.py and improve maintainability.
- Add validation for uploaded attachments (e.g., file size, mime type) to prevent users from sending excessively large or unsupported files.
- The admin orders list currently renders all items at once; consider implementing pagination or dynamic loading to manage large numbers of orders more effectively.

## Individual Comments

### Comment 1
<location> `bot.py:77-86` </location>
<code_context>
+def format_contact_link(contact: str) -> str:
</code_context>

<issue_to_address>
**suggestion:** Consider handling contacts with non-standard characters more robustly.

Telegram usernames may include numbers and are case-insensitive, but your current validation excludes some valid characters like dashes or periods. Consider updating the allowed character set or notifying users when their input is invalid.
</issue_to_address>

### Comment 2
<location> `bot.py:99-108` </location>
<code_context>
+def build_order_details(uid: str, order: dict) -> str:
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Escape upsell titles to prevent HTML injection.

Escape each upsell title individually before joining to avoid malformed output if titles contain HTML special characters.

Suggested implementation:

```python
def build_order_details(uid: str, order: dict) -> str:
    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)
    upsells = order.get('upsells', [])
    escaped_upsell_titles = [escape(str(title)) for title in upsells]
    upsell_titles_str = ', '.join(escaped_upsell_titles) if escaped_upsell_titles else 'Нет'
    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"Упселлы: {upsell_titles_str}",

```

If the upsell titles are displayed elsewhere or if the upsells field is not a list of titles, you may need to adjust the extraction logic accordingly. Also, ensure that the `escape` function is imported from the correct module (e.g., `from html import escape`) at the top of the file if not already present.
</issue_to_address>

### Comment 3
<location> `bot.py:371-380` </location>
<code_context>
+    return UPLOAD_FILES
+
+async def skip_files(update: Update, context: ContextTypes.DEFAULT_TYPE):
+    context.user_data['current_files'] = []
+    await update.message.reply_text("Пропускаем прикрепление файлов.")
+    return await request_contact(update, context)
+
+async def finish_files(update: Update, context: ContextTypes.DEFAULT_TYPE):
</code_context>

<issue_to_address>
**issue (performance):** Potential for large file uploads without size or count limits.

Currently, there are no checks on file size or count, which may cause performance or storage problems. Please implement limits and notify users when these are exceeded.
</issue_to_address>

### Comment 4
<location> `bot.py:445-452` </location>
<code_context>
+    return INPUT_CONTACT
+
+async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
+    contact = update.message.text.strip()
+    if not contact:
+        await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
+        return INPUT_CONTACT
+    context.user_data['current_contact'] = contact
+    context.user_data['last_contact'] = contact
+    return await add_upsell(update, context)
+
 # Добавление допуслуг
</code_context>

<issue_to_address>
**suggestion:** Contact field validation may be too permissive.

The current implementation accepts any non-empty string as contact information, which may allow invalid entries. Please implement validation for expected contact formats and notify users when their input is invalid.

```suggestion
import re

def is_valid_contact(contact: str) -> bool:
    # Email
    email_pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"
    # Phone (simple international and local formats)
    phone_pattern = r"^\+?\d{10,15}$"
    # Telegram username
    telegram_pattern = r"^@[\w\d_]{5,}$"
    return (
        re.match(email_pattern, contact)
        or re.match(phone_pattern, contact)
        or re.match(telegram_pattern, contact)
    )

async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
    contact = update.message.text.strip()
    if not contact:
        await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
        return INPUT_CONTACT
    if not is_valid_contact(contact):
        await update.message.reply_text(
            "Пожалуйста, укажите корректный контакт: email, номер телефона (10-15 цифр, можно с +), или Telegram username (@username)."
        )
        return INPUT_CONTACT
    context.user_data['current_contact'] = contact
    context.user_data['last_contact'] = contact
    return await add_upsell(update, context)
```
</issue_to_address>

### Comment 5
<location> `bot.py:667` </location>
<code_context>
     return await show_admin_menu(update, context)

 # Обработчик админ-меню
-async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
-    query = update.callback_query
-    await query.answer()
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring large handler cascades and file/contact flows into separate modules with dispatch maps and conversation handlers.

```markdown
You’ve done great work adding file-upload/contact flows and richer admin screens, but `bot.py` is now ~1 200 LOC with two huge `if/elif` cascades and even a duplicated `add_upsell()` definition. Two quick wins:

1) **Extract & dispatch admin callbacks**  
   Replace the big `admin_menu_handler` `if/elif` tree with a small dispatch map + per-action functions. E.g.:

   ```python
   # admin.py
   from telegram import InlineKeyboardMarkup, InlineKeyboardButton
   from telegram.constants import ParseMode

   async def list_orders(update, context):
       query = update.callback_query
       await query.answer()
       buttons = []
       for uid, orders in ORDERS.items():
           for o in orders:
               buttons.append([
                   InlineKeyboardButton(
                       f"#{o['order_id']} ({uid})",
                       callback_data=f"admin_order|{uid}|{o['order_id']}"
                   )
               ])
       buttons.append([InlineKeyboardButton("Назад", callback_data="admin_menu")])
       await query.edit_message_text(
           "📋 Заказы:\n" + ("\n".join(f"#{o['order_id']} от {uid}: {o['status']}"
                                      for uid, orders in ORDERS.items() for o in orders)
                             or "Пусто"),
           reply_markup=InlineKeyboardMarkup(buttons)
       )
       return ADMIN_MENU

   async def view_order(update, context, uid, oid):
       query = update.callback_query
       await query.answer()
       order = next((o for o in ORDERS.get(uid, []) if str(o['order_id'])==oid), None)
       text = build_order_details(uid, order) if order else "Заказ не найден."
       kb = [[InlineKeyboardButton("⬅️ Назад", callback_data="admin_orders")]]
       await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(kb), parse_mode=ParseMode.HTML)
       return ADMIN_MENU

   # … similarly: cancel_order(), delete_order()

   ADMIN_ACTIONS = {
     "admin_orders": list_orders,
     "admin_order":  view_order,
     "admin_cancel": cancel_order,
     "admin_delete": delete_order,
   }

   async def admin_menu_handler(update, context):
       data = (await update.callback_query).data
       key, *args = data.split("|")
       handler = ADMIN_ACTIONS.get(key)
       if handler:
           return await handler(update, context, *args)
       return await show_admin_menu(update, context)
   ```

   In your main file just import `admin_menu_handler` and remove the big cascade.

2) **Move file-upload/contact steps into their own module**  
   Right now you have 6 functions in a row for UPLOAD_FILESINPUT_CONTACT. Bundle them into a mini `ConversationHandler` in `file_flow.py` and register it separately:

   ```python
   # file_flow.py
   from telegram.ext import (
     ConversationHandler, MessageHandler, CommandHandler, filters, CallbackQueryHandler
   )

   states = {
     'UPLOAD_FILES': [  # use your UPLOAD_FILES constant
       MessageHandler(filters.Document.ALL, handle_document_upload),
       MessageHandler(filters.PHOTO,      handle_photo_upload),
       #
     ],
     'INPUT_CONTACT': [
       MessageHandler(filters.TEXT & ~filters.COMMAND, input_contact),
     ],
   }

   file_upload_flow = ConversationHandler(
     entry_points=[CallbackQueryHandler(prompt_file_upload, pattern='^add_upsell$')],
     states=states,
     fallbacks=[
       CommandHandler('skip', skip_files),
       CommandHandler('done', finish_files),
     ]
   )
   ```

   Then in your setup:
   ```python
   from file_flow import file_upload_flow
   application.add_handler(file_upload_flow)
   ```

3) **Remove the duplicate `add_upsell()`** – consolidate its logic into one place.

These steps will shrink `bot.py` by hundreds of lines, improve readability, and make each flow easier to test.
</issue_to_address>

### Comment 6
<location> `bot.py:634` </location>
<code_context>
                    f"Требования: {requirements if requirements else 'Нет'}",

</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace if-expression with `or` ([`or-if-exp-identity`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/or-if-exp-identity))

```suggestion
                    f"Требования: {requirements or 'Нет'}",
```

<br/><details><summary>Explanation</summary>Here we find ourselves setting a value if it evaluates to `True`, and otherwise
using a default.

The 'After' case is a bit easier to read and avoids the duplication of
`input_currency`.

It works because the left-hand side is evaluated first. If it evaluates to
true then `currency` will be set to this and the right-hand side will not be
evaluated. If it evaluates to false the right-hand side will be evaluated and
`currency` will be set to `DEFAULT_CURRENCY`.
</details>
</issue_to_address>

### Comment 7
<location> `bot.py:105` </location>
<code_context>
def build_order_details(uid: str, order: dict) -> str:
    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"Контакт: {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 [])}")
    if order.get('created_at'):
        lines.append(f"Создан: {escape(order['created_at'])}")
    return '<br>'.join(lines)

</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
        f'Пользователь: <a href=\"{user_link}\">{escape(uid)}</a>',
```
</issue_to_address>

### Comment 8
<location> `bot.py:434-435` </location>
<code_context>
async def request_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
    context.user_data.pop('current_contact', None)
    prompt = (
        "Укажите контакт, куда менеджеру написать (Telegram, ВКонтакте, почта). Это обязательное поле."
    )
    last_contact = context.user_data.get('last_contact')
    if last_contact:
        prompt += f"\nРанее вы указывали: {last_contact}. Можно отправить его снова или написать другой."
    if update.message:
        await update.message.reply_text(prompt)
    elif update.callback_query:
        query = update.callback_query
        await query.answer()
        await query.edit_message_text(prompt)
    return INPUT_CONTACT

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
    if last_contact := context.user_data.get('last_contact'):
```
</issue_to_address>

### Comment 9
<location> `bot.py:888` </location>
<code_context>
async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    data = query.data
    if data == 'admin_menu':
        return await show_admin_menu(update, context)
    if data == 'admin_orders':
        text_lines = []
        buttons = []
        for uid, ords in ORDERS.items():
            for ord_data in ords:
                text_lines.append(f"#{ord_data.get('order_id', 'N/A')} от {uid}: {ord_data.get('status', 'новый')}")
                buttons.append([
                    InlineKeyboardButton(
                        f"#{ord_data.get('order_id', 'N/A')} ({uid})",
                        callback_data=f"admin_order|{uid}|{ord_data.get('order_id', 'N/A')}"
                    )
                ])
        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))
        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)
        keyboard = [
            [InlineKeyboardButton("Отменить заказ", callback_data=f'admin_cancel|{uid}|{order_id_str}')],
            [InlineKeyboardButton("Удалить заказ", callback_data=f'admin_delete|{uid}|{order_id_str}')],
            [InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')]
        ]
        await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), 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 = "📤 Экспорт отправлен!"
    elif data == 'back_to_main':
        return await main_menu(update, context)
    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 - 12% ([`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></s>

</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>

<details>
<summary>Original comment in English</summary>

Hey there - I've reviewed your changes - here's some feedback:

- Consider refactoring the file upload and contact collection logic into dedicated modules or classes to reduce the size of bot.py and improve maintainability.
- Add validation for uploaded attachments (e.g., file size, mime type) to prevent users from sending excessively large or unsupported files.
- The admin orders list currently renders all items at once; consider implementing pagination or dynamic loading to manage large numbers of orders more effectively.

<details>
<summary>Prompt for AI Agents</summary>

~~~markdown
Please address the comments from this code review:

## Overall Comments
- Consider refactoring the file upload and contact collection logic into dedicated modules or classes to reduce the size of bot.py and improve maintainability.
- Add validation for uploaded attachments (e.g., file size, mime type) to prevent users from sending excessively large or unsupported files.
- The admin orders list currently renders all items at once; consider implementing pagination or dynamic loading to manage large numbers of orders more effectively.

## Individual Comments

### Comment 1
<location> `bot.py:77-86` </location>
<code_context>
+def format_contact_link(contact: str) -> str:
</code_context>

<issue_to_address>
**suggestion:** Consider handling contacts with non-standard characters more robustly.

Telegram usernames may include numbers and are case-insensitive, but your current validation excludes some valid characters like dashes or periods. Consider updating the allowed character set or notifying users when their input is invalid.
</issue_to_address>

### Comment 2
<location> `bot.py:99-108` </location>
<code_context>
+def build_order_details(uid: str, order: dict) -> str:
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Escape upsell titles to prevent HTML injection.

Escape each upsell title individually before joining to avoid malformed output if titles contain HTML special characters.

Suggested implementation:

```python
def build_order_details(uid: str, order: dict) -> str:
    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)
    upsells = order.get('upsells', [])
    escaped_upsell_titles = [escape(str(title)) for title in upsells]
    upsell_titles_str = ', '.join(escaped_upsell_titles) if escaped_upsell_titles else 'Нет'
    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"Упселлы: {upsell_titles_str}",

```

If the upsell titles are displayed elsewhere or if the upsells field is not a list of titles, you may need to adjust the extraction logic accordingly. Also, ensure that the `escape` function is imported from the correct module (e.g., `from html import escape`) at the top of the file if not already present.
</issue_to_address>

### Comment 3
<location> `bot.py:371-380` </location>
<code_context>
+    return UPLOAD_FILES
+
+async def skip_files(update: Update, context: ContextTypes.DEFAULT_TYPE):
+    context.user_data['current_files'] = []
+    await update.message.reply_text("Пропускаем прикрепление файлов.")
+    return await request_contact(update, context)
+
+async def finish_files(update: Update, context: ContextTypes.DEFAULT_TYPE):
</code_context>

<issue_to_address>
**issue (performance):** Potential for large file uploads without size or count limits.

Currently, there are no checks on file size or count, which may cause performance or storage problems. Please implement limits and notify users when these are exceeded.
</issue_to_address>

### Comment 4
<location> `bot.py:445-452` </location>
<code_context>
+    return INPUT_CONTACT
+
+async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
+    contact = update.message.text.strip()
+    if not contact:
+        await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
+        return INPUT_CONTACT
+    context.user_data['current_contact'] = contact
+    context.user_data['last_contact'] = contact
+    return await add_upsell(update, context)
+
 # Добавление допуслуг
</code_context>

<issue_to_address>
**suggestion:** Contact field validation may be too permissive.

The current implementation accepts any non-empty string as contact information, which may allow invalid entries. Please implement validation for expected contact formats and notify users when their input is invalid.

```suggestion
import re

def is_valid_contact(contact: str) -> bool:
    # Email
    email_pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"
    # Phone (simple international and local formats)
    phone_pattern = r"^\+?\d{10,15}$"
    # Telegram username
    telegram_pattern = r"^@[\w\d_]{5,}$"
    return (
        re.match(email_pattern, contact)
        or re.match(phone_pattern, contact)
        or re.match(telegram_pattern, contact)
    )

async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
    contact = update.message.text.strip()
    if not contact:
        await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
        return INPUT_CONTACT
    if not is_valid_contact(contact):
        await update.message.reply_text(
            "Пожалуйста, укажите корректный контакт: email, номер телефона (10-15 цифр, можно с +), или Telegram username (@username)."
        )
        return INPUT_CONTACT
    context.user_data['current_contact'] = contact
    context.user_data['last_contact'] = contact
    return await add_upsell(update, context)
```
</issue_to_address>

### Comment 5
<location> `bot.py:667` </location>
<code_context>
     return await show_admin_menu(update, context)

 # Обработчик админ-меню
-async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
-    query = update.callback_query
-    await query.answer()
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring large handler cascades and file/contact flows into separate modules with dispatch maps and conversation handlers.

```markdown
You’ve done great work adding file-upload/contact flows and richer admin screens, but `bot.py` is now ~1 200 LOC with two huge `if/elif` cascades and even a duplicated `add_upsell()` definition. Two quick wins:

1) **Extract & dispatch admin callbacks**  
   Replace the big `admin_menu_handler` `if/elif` tree with a small dispatch map + per-action functions. E.g.:

   ```python
   # admin.py
   from telegram import InlineKeyboardMarkup, InlineKeyboardButton
   from telegram.constants import ParseMode

   async def list_orders(update, context):
       query = update.callback_query
       await query.answer()
       buttons = []
       for uid, orders in ORDERS.items():
           for o in orders:
               buttons.append([
                   InlineKeyboardButton(
                       f"#{o['order_id']} ({uid})",
                       callback_data=f"admin_order|{uid}|{o['order_id']}"
                   )
               ])
       buttons.append([InlineKeyboardButton("Назад", callback_data="admin_menu")])
       await query.edit_message_text(
           "📋 Заказы:\n" + ("\n".join(f"#{o['order_id']} от {uid}: {o['status']}"
                                      for uid, orders in ORDERS.items() for o in orders)
                             or "Пусто"),
           reply_markup=InlineKeyboardMarkup(buttons)
       )
       return ADMIN_MENU

   async def view_order(update, context, uid, oid):
       query = update.callback_query
       await query.answer()
       order = next((o for o in ORDERS.get(uid, []) if str(o['order_id'])==oid), None)
       text = build_order_details(uid, order) if order else "Заказ не найден."
       kb = [[InlineKeyboardButton("⬅️ Назад", callback_data="admin_orders")]]
       await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(kb), parse_mode=ParseMode.HTML)
       return ADMIN_MENU

   # … similarly: cancel_order(), delete_order()

   ADMIN_ACTIONS = {
     "admin_orders": list_orders,
     "admin_order":  view_order,
     "admin_cancel": cancel_order,
     "admin_delete": delete_order,
   }

   async def admin_menu_handler(update, context):
       data = (await update.callback_query).data
       key, *args = data.split("|")
       handler = ADMIN_ACTIONS.get(key)
       if handler:
           return await handler(update, context, *args)
       return await show_admin_menu(update, context)
   ```

   In your main file just import `admin_menu_handler` and remove the big cascade.

2) **Move file-upload/contact steps into their own module**  
   Right now you have 6 functions in a row for UPLOAD_FILESINPUT_CONTACT. Bundle them into a mini `ConversationHandler` in `file_flow.py` and register it separately:

   ```python
   # file_flow.py
   from telegram.ext import (
     ConversationHandler, MessageHandler, CommandHandler, filters, CallbackQueryHandler
   )

   states = {
     'UPLOAD_FILES': [  # use your UPLOAD_FILES constant
       MessageHandler(filters.Document.ALL, handle_document_upload),
       MessageHandler(filters.PHOTO,      handle_photo_upload),
       #
     ],
     'INPUT_CONTACT': [
       MessageHandler(filters.TEXT & ~filters.COMMAND, input_contact),
     ],
   }

   file_upload_flow = ConversationHandler(
     entry_points=[CallbackQueryHandler(prompt_file_upload, pattern='^add_upsell$')],
     states=states,
     fallbacks=[
       CommandHandler('skip', skip_files),
       CommandHandler('done', finish_files),
     ]
   )
   ```

   Then in your setup:
   ```python
   from file_flow import file_upload_flow
   application.add_handler(file_upload_flow)
   ```

3) **Remove the duplicate `add_upsell()`** – consolidate its logic into one place.

These steps will shrink `bot.py` by hundreds of lines, improve readability, and make each flow easier to test.
</issue_to_address>

### Comment 6
<location> `bot.py:634` </location>
<code_context>
                    f"Требования: {requirements if requirements else 'Нет'}",

</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace if-expression with `or` ([`or-if-exp-identity`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/or-if-exp-identity))

```suggestion
                    f"Требования: {requirements or 'Нет'}",
```

<br/><details><summary>Explanation</summary>Here we find ourselves setting a value if it evaluates to `True`, and otherwise
using a default.

The 'After' case is a bit easier to read and avoids the duplication of
`input_currency`.

It works because the left-hand side is evaluated first. If it evaluates to
true then `currency` will be set to this and the right-hand side will not be
evaluated. If it evaluates to false the right-hand side will be evaluated and
`currency` will be set to `DEFAULT_CURRENCY`.
</details>
</issue_to_address>

### Comment 7
<location> `bot.py:105` </location>
<code_context>
def build_order_details(uid: str, order: dict) -> str:
    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"Контакт: {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 [])}")
    if order.get('created_at'):
        lines.append(f"Создан: {escape(order['created_at'])}")
    return '<br>'.join(lines)

</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
        f'Пользователь: <a href=\"{user_link}\">{escape(uid)}</a>',
```
</issue_to_address>

### Comment 8
<location> `bot.py:434-435` </location>
<code_context>
async def request_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
    context.user_data.pop('current_contact', None)
    prompt = (
        "Укажите контакт, куда менеджеру написать (Telegram, ВКонтакте, почта). Это обязательное поле."
    )
    last_contact = context.user_data.get('last_contact')
    if last_contact:
        prompt += f"\nРанее вы указывали: {last_contact}. Можно отправить его снова или написать другой."
    if update.message:
        await update.message.reply_text(prompt)
    elif update.callback_query:
        query = update.callback_query
        await query.answer()
        await query.edit_message_text(prompt)
    return INPUT_CONTACT

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
    if last_contact := context.user_data.get('last_contact'):
```
</issue_to_address>

### Comment 9
<location> `bot.py:888` </location>
<code_context>
async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    data = query.data
    if data == 'admin_menu':
        return await show_admin_menu(update, context)
    if data == 'admin_orders':
        text_lines = []
        buttons = []
        for uid, ords in ORDERS.items():
            for ord_data in ords:
                text_lines.append(f"#{ord_data.get('order_id', 'N/A')} от {uid}: {ord_data.get('status', 'новый')}")
                buttons.append([
                    InlineKeyboardButton(
                        f"#{ord_data.get('order_id', 'N/A')} ({uid})",
                        callback_data=f"admin_order|{uid}|{ord_data.get('order_id', 'N/A')}"
                    )
                ])
        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))
        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)
        keyboard = [
            [InlineKeyboardButton("Отменить заказ", callback_data=f'admin_cancel|{uid}|{order_id_str}')],
            [InlineKeyboardButton("Удалить заказ", callback_data=f'admin_delete|{uid}|{order_id_str}')],
            [InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')]
        ]
        await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), 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 = "📤 Экспорт отправлен!"
    elif data == 'back_to_main':
        return await main_menu(update, context)
    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 - 12% ([`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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread bot.py
Comment on lines +77 to +86
def format_contact_link(contact: str) -> str:
if not contact:
return 'не указан'
contact = contact.strip()
if not contact:
return 'не указан'
safe_display = escape(contact)
if contact.startswith('@') and len(contact) > 1:
username = contact[1:].split()[0]
if username and all(ch.isalnum() or ch == '_' for ch in username):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Рассмотрите возможность более надежной обработки контактов с нестандартными символами.

Имена пользователей Telegram могут содержать цифры и нечувствительны к регистру, но ваша текущая проверка исключает некоторые допустимые символы, такие как дефисы или точки. Рассмотрите возможность обновления разрешенного набора символов или уведомления пользователей, если их ввод недействителен.

Original comment in English

suggestion: Consider handling contacts with non-standard characters more robustly.

Telegram usernames may include numbers and are case-insensitive, but your current validation excludes some valid characters like dashes or periods. Consider updating the allowed character set or notifying users when their input is invalid.

Comment thread bot.py
Comment on lines +99 to +108
def build_order_details(uid: str, order: dict) -> str:
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')} дней",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Экранируйте заголовки допродаж, чтобы предотвратить внедрение HTML-кода.

Экранируйте каждый заголовок допродажи по отдельности перед объединением, чтобы избежать некорректного вывода, если заголовки содержат специальные символы HTML.

Предлагаемая реализация:

def build_order_details(uid: str, order: dict) -> str:
    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)
    upsells = order.get('upsells', [])
    escaped_upsell_titles = [escape(str(title)) for title in upsells]
    upsell_titles_str = ', '.join(escaped_upsell_titles) if escaped_upsell_titles else 'Нет'
    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"Упселлы: {upsell_titles_str}",

Если заголовки допродаж отображаются в других местах или если поле допродаж не является списком заголовков, вам может потребоваться соответствующая корректировка логики извлечения. Также убедитесь, что функция escape импортирована из правильного модуля (например, from html import escape) в начале файла, если она еще не присутствует.

Original comment in English

🚨 suggestion (security): Escape upsell titles to prevent HTML injection.

Escape each upsell title individually before joining to avoid malformed output if titles contain HTML special characters.

Suggested implementation:

def build_order_details(uid: str, order: dict) -> str:
    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)
    upsells = order.get('upsells', [])
    escaped_upsell_titles = [escape(str(title)) for title in upsells]
    upsell_titles_str = ', '.join(escaped_upsell_titles) if escaped_upsell_titles else 'Нет'
    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"Упселлы: {upsell_titles_str}",

If the upsell titles are displayed elsewhere or if the upsells field is not a list of titles, you may need to adjust the extraction logic accordingly. Also, ensure that the escape function is imported from the correct module (e.g., from html import escape) at the top of the file if not already present.

Comment thread bot.py
Comment on lines +371 to +380
context.user_data['current_files'] = []
context.user_data.pop('current_contact', None)
text = (
"Прикрепите файлы для заказа (если они есть). Отправьте все документы подряд.\n"
"Когда закончите, нажмите /done. Если файлов нет, нажмите /skip."
)
if update.message:
await update.message.reply_text(text)
elif update.callback_query:
query = update.callback_query

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (performance): Возможность загрузки больших файлов без ограничений по размеру или количеству.

В настоящее время отсутствуют проверки размера или количества файлов, что может привести к проблемам с производительностью или хранением. Пожалуйста, реализуйте ограничения и уведомляйте пользователей о их превышении.

Original comment in English

issue (performance): Potential for large file uploads without size or count limits.

Currently, there are no checks on file size or count, which may cause performance or storage problems. Please implement limits and notify users when these are exceeded.

Comment thread bot.py
Comment on lines +445 to +452
async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
contact = update.message.text.strip()
if not contact:
await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
return INPUT_CONTACT
context.user_data['current_contact'] = contact
context.user_data['last_contact'] = contact
return await add_upsell(update, context)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Проверка поля контакта может быть слишком свободной.

Текущая реализация принимает любую непустую строку в качестве контактной информации, что может допускать неверные записи. Пожалуйста, реализуйте проверку ожидаемых форматов контактов и уведомляйте пользователей, когда их ввод недействителен.

Suggested change
async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
contact = update.message.text.strip()
if not contact:
await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
return INPUT_CONTACT
context.user_data['current_contact'] = contact
context.user_data['last_contact'] = contact
return await add_upsell(update, context)
import re
def is_valid_contact(contact: str) -> bool:
# Email
email_pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"
# Phone (simple international and local formats)
phone_pattern = r"^\+?\d{10,15}$"
# Telegram username
telegram_pattern = r"^@[\w\d_]{5,}$"
return (
re.match(email_pattern, contact)
or re.match(phone_pattern, contact)
or re.match(telegram_pattern, contact)
)
async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
contact = update.message.text.strip()
if not contact:
await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
return INPUT_CONTACT
if not is_valid_contact(contact):
await update.message.reply_text(
"Пожалуйста, укажите корректный контакт: email, номер телефона (10-15 цифр, можно с +), или Telegram username (@username)."
)
return INPUT_CONTACT
context.user_data['current_contact'] = contact
context.user_data['last_contact'] = contact
return await add_upsell(update, context)
Original comment in English

suggestion: Contact field validation may be too permissive.

The current implementation accepts any non-empty string as contact information, which may allow invalid entries. Please implement validation for expected contact formats and notify users when their input is invalid.

Suggested change
async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
contact = update.message.text.strip()
if not contact:
await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
return INPUT_CONTACT
context.user_data['current_contact'] = contact
context.user_data['last_contact'] = contact
return await add_upsell(update, context)
import re
def is_valid_contact(contact: str) -> bool:
# Email
email_pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"
# Phone (simple international and local formats)
phone_pattern = r"^\+?\d{10,15}$"
# Telegram username
telegram_pattern = r"^@[\w\d_]{5,}$"
return (
re.match(email_pattern, contact)
or re.match(phone_pattern, contact)
or re.match(telegram_pattern, contact)
)
async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE):
contact = update.message.text.strip()
if not contact:
await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.")
return INPUT_CONTACT
if not is_valid_contact(contact):
await update.message.reply_text(
"Пожалуйста, укажите корректный контакт: email, номер телефона (10-15 цифр, можно с +), или Telegram username (@username)."
)
return INPUT_CONTACT
context.user_data['current_contact'] = contact
context.user_data['last_contact'] = contact
return await add_upsell(update, context)

Comment thread bot.py
caption=caption,
parse_mode=ParseMode.HTML
)
else:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Рассмотрите возможность рефакторинга больших каскадов обработчиков и потоков файлов/контактов в отдельные модули с картами диспетчеризации и обработчиками диалогов.

You’ve done great work adding file-upload/contact flows and richer admin screens, but `bot.py` is now ~1 200 LOC with two huge `if/elif` cascades and even a duplicated `add_upsell()` definition. Two quick wins:

1) **Extract & dispatch admin callbacks**  
   Replace the big `admin_menu_handler` `if/elif` tree with a small dispatch map + per-action functions. E.g.:

   ```python
   # admin.py
   from telegram import InlineKeyboardMarkup, InlineKeyboardButton
   from telegram.constants import ParseMode

   async def list_orders(update, context):
       query = update.callback_query
       await query.answer()
       buttons = []
       for uid, orders in ORDERS.items():
           for o in orders:
               buttons.append([
                   InlineKeyboardButton(
                       f"#{o['order_id']} ({uid})",
                       callback_data=f"admin_order|{uid}|{o['order_id']}"
                   )
               ])
       buttons.append([InlineKeyboardButton("Назад", callback_data="admin_menu")])
       await query.edit_message_text(
           "📋 Заказы:\n" + ("\n".join(f"#{o['order_id']} от {uid}: {o['status']}"
                                      for uid, orders in ORDERS.items() for o in orders)
                             or "Пусто"),
           reply_markup=InlineKeyboardMarkup(buttons)
       )
       return ADMIN_MENU

   async def view_order(update, context, uid, oid):
       query = update.callback_query
       await query.answer()
       order = next((o for o in ORDERS.get(uid, []) if str(o['order_id'])==oid), None)
       text = build_order_details(uid, order) if order else "Заказ не найден."
       kb = [[InlineKeyboardButton("⬅️ Назад", callback_data="admin_orders")]]
       await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(kb), parse_mode=ParseMode.HTML)
       return ADMIN_MENU

   # … similarly: cancel_order(), delete_order()

   ADMIN_ACTIONS = {
     "admin_orders": list_orders,
     "admin_order":  view_order,
     "admin_cancel": cancel_order,
     "admin_delete": delete_order,
   }

   async def admin_menu_handler(update, context):
       data = (await update.callback_query).data
       key, *args = data.split("|")
       handler = ADMIN_ACTIONS.get(key)
       if handler:
           return await handler(update, context, *args)
       return await show_admin_menu(update, context)

In your main file just import admin_menu_handler and remove the big cascade.

  1. Move file-upload/contact steps into their own module
    Right now you have 6 functions in a row for UPLOAD_FILES→INPUT_CONTACT. Bundle them into a mini ConversationHandler in file_flow.py and register it separately:

    # file_flow.py
    from telegram.ext import (
      ConversationHandler, MessageHandler, CommandHandler, filters, CallbackQueryHandler
    )
    
    states = {
      'UPLOAD_FILES': [  # use your UPLOAD_FILES constant
        MessageHandler(filters.Document.ALL, handle_document_upload),
        MessageHandler(filters.PHOTO,      handle_photo_upload),
        # …
      ],
      'INPUT_CONTACT': [
        MessageHandler(filters.TEXT & ~filters.COMMAND, input_contact),
      ],
    }
    
    file_upload_flow = ConversationHandler(
      entry_points=[CallbackQueryHandler(prompt_file_upload, pattern='^add_upsell$')],
      states=states,
      fallbacks=[
        CommandHandler('skip', skip_files),
        CommandHandler('done', finish_files),
      ]
    )

    Then in your setup:

    from file_flow import file_upload_flow
    application.add_handler(file_upload_flow)
  2. Remove the duplicate add_upsell() – consolidate its logic into one place.

These steps will shrink bot.py by hundreds of lines, improve readability, and make each flow easier to test.

Original comment in English

issue (complexity): Consider refactoring large handler cascades and file/contact flows into separate modules with dispatch maps and conversation handlers.

You’ve done great work adding file-upload/contact flows and richer admin screens, but `bot.py` is now ~1 200 LOC with two huge `if/elif` cascades and even a duplicated `add_upsell()` definition. Two quick wins:

1) **Extract & dispatch admin callbacks**  
   Replace the big `admin_menu_handler` `if/elif` tree with a small dispatch map + per-action functions. E.g.:

   ```python
   # admin.py
   from telegram import InlineKeyboardMarkup, InlineKeyboardButton
   from telegram.constants import ParseMode

   async def list_orders(update, context):
       query = update.callback_query
       await query.answer()
       buttons = []
       for uid, orders in ORDERS.items():
           for o in orders:
               buttons.append([
                   InlineKeyboardButton(
                       f"#{o['order_id']} ({uid})",
                       callback_data=f"admin_order|{uid}|{o['order_id']}"
                   )
               ])
       buttons.append([InlineKeyboardButton("Назад", callback_data="admin_menu")])
       await query.edit_message_text(
           "📋 Заказы:\n" + ("\n".join(f"#{o['order_id']} от {uid}: {o['status']}"
                                      for uid, orders in ORDERS.items() for o in orders)
                             or "Пусто"),
           reply_markup=InlineKeyboardMarkup(buttons)
       )
       return ADMIN_MENU

   async def view_order(update, context, uid, oid):
       query = update.callback_query
       await query.answer()
       order = next((o for o in ORDERS.get(uid, []) if str(o['order_id'])==oid), None)
       text = build_order_details(uid, order) if order else "Заказ не найден."
       kb = [[InlineKeyboardButton("⬅️ Назад", callback_data="admin_orders")]]
       await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(kb), parse_mode=ParseMode.HTML)
       return ADMIN_MENU

   # … similarly: cancel_order(), delete_order()

   ADMIN_ACTIONS = {
     "admin_orders": list_orders,
     "admin_order":  view_order,
     "admin_cancel": cancel_order,
     "admin_delete": delete_order,
   }

   async def admin_menu_handler(update, context):
       data = (await update.callback_query).data
       key, *args = data.split("|")
       handler = ADMIN_ACTIONS.get(key)
       if handler:
           return await handler(update, context, *args)
       return await show_admin_menu(update, context)

In your main file just import admin_menu_handler and remove the big cascade.

  1. Move file-upload/contact steps into their own module
    Right now you have 6 functions in a row for UPLOAD_FILES→INPUT_CONTACT. Bundle them into a mini ConversationHandler in file_flow.py and register it separately:

    # file_flow.py
    from telegram.ext import (
      ConversationHandler, MessageHandler, CommandHandler, filters, CallbackQueryHandler
    )
    
    states = {
      'UPLOAD_FILES': [  # use your UPLOAD_FILES constant
        MessageHandler(filters.Document.ALL, handle_document_upload),
        MessageHandler(filters.PHOTO,      handle_photo_upload),
        # …
      ],
      'INPUT_CONTACT': [
        MessageHandler(filters.TEXT & ~filters.COMMAND, input_contact),
      ],
    }
    
    file_upload_flow = ConversationHandler(
      entry_points=[CallbackQueryHandler(prompt_file_upload, pattern='^add_upsell$')],
      states=states,
      fallbacks=[
        CommandHandler('skip', skip_files),
        CommandHandler('done', finish_files),
      ]
    )

    Then in your setup:

    from file_flow import file_upload_flow
    application.add_handler(file_upload_flow)
  2. Remove the duplicate add_upsell() – consolidate its logic into one place.

These steps will shrink bot.py by hundreds of lines, improve readability, and make each flow easier to test.

Comment thread bot.py
f"<b>#{order['order_id']}</b> {escape(order_name)} — {topic}",
f"Срок: {order.get('deadline_days', 'N/A')} дней | Цена: {order.get('price', 0)} ₽",
f"Контакт: {contact_html}",
f"Требования: {requirements if requirements else 'Нет'}",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Замените if-выражение на or (or-if-exp-identity)

Suggested change
f"Требования: {requirements if requirements else 'Нет'}",
f"Требования: {requirements or 'Нет'}",


ExplanationЗдесь мы устанавливаем значение, если оно оценивается как True, в противном случае
используя значение по умолчанию.

Случай 'После' немного легче читается и позволяет избежать дублирования
input_currency.

Это работает, потому что левая часть оценивается первой. Если она оценивается как
истинная, то currency будет установлено в это значение, а правая часть не будет
оцениваться. Если она оценивается как ложная, то правая часть будет оцениваться, и
currency будет установлено в DEFAULT_CURRENCY.

Original comment in English

suggestion (code-quality): Replace if-expression with or (or-if-exp-identity)

Suggested change
f"Требования: {requirements if requirements else 'Нет'}",
f"Требования: {requirements or 'Нет'}",


ExplanationHere we find ourselves setting a value if it evaluates to True, and otherwise
using a default.

The 'After' case is a bit easier to read and avoids the duplication of
input_currency.

It works because the left-hand side is evaluated first. If it evaluates to
true then currency will be set to this and the right-hand side will not be
evaluated. If it evaluates to false the right-hand side will be evaluated and
currency will be set to DEFAULT_CURRENCY.

Comment thread bot.py
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>",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Удалите ненужные приведения к int, str, float или bool (remove-unnecessary-cast)

Suggested change
f"Пользователь: <a href=\"{user_link}\">{escape(str(uid))}</a>",
f'Пользователь: <a href=\"{user_link}\">{escape(uid)}</a>',
Original comment in English

suggestion (code-quality): Remove unnecessary casts to int, str, float or bool (remove-unnecessary-cast)

Suggested change
f"Пользователь: <a href=\"{user_link}\">{escape(str(uid))}</a>",
f'Пользователь: <a href=\"{user_link}\">{escape(uid)}</a>',

Comment thread bot.py
Comment on lines +434 to +435
last_contact = context.user_data.get('last_contact')
if last_contact:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Используйте именованные выражения для упрощения присваивания и условного оператора (use-named-expression)

Suggested change
last_contact = context.user_data.get('last_contact')
if last_contact:
if last_contact := context.user_data.get('last_contact'):
Original comment in English

suggestion (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)

Suggested change
last_contact = context.user_data.get('last_contact')
if last_contact:
if last_contact := context.user_data.get('last_contact'):

Comment thread bot.py
text += f"#{ord['order_id']} от {uid}: {ord['status']}\n"
elif data == 'admin_users':
text = "👥 Пользователи:\n" + "\n".join(f"ID: {uid}" for uid in ORDERS.keys())
async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Низкое качество кода обнаружено в admin_menu_handler - 12% (low-code-quality)


ExplanationОценка качества для этой функции ниже порогового значения качества в 25%.
Эта оценка представляет собой комбинацию длины метода, когнитивной сложности и рабочей памяти.

Как вы можете это решить?

Возможно, стоит провести рефакторинг этой функции, чтобы сделать ее короче и читабельнее.

  • Уменьшите длину функции, извлекая части функциональности в
    отдельные функции. Это самое важное, что вы можете сделать — в идеале функция
    должна быть менее 10 строк.
  • Уменьшите вложенность, возможно, путем введения защитных условий для раннего выхода.
  • Убедитесь, что переменные имеют строго ограниченную область видимости, чтобы код, использующий связанные концепции,
    находился вместе внутри функции, а не был разбросан.
Original comment in English

issue (code-quality): Low code quality found in admin_menu_handler - 12% (low-code-quality)


ExplanationThe 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant