aboutsummaryrefslogtreecommitdiff
path: root/handlers
diff options
context:
space:
mode:
authorTolmachev Igor <me@igorek.dev>2026-04-20 20:56:35 +0300
committerTolmachev Igor <me@igorek.dev>2026-04-20 20:56:35 +0300
commit75e99ca0712a2c09230e5c6f8d093dc526cc717d (patch)
treef3f00494364a82b866f093651cb9a08030135c4e /handlers
parentf186fca0f1aa9bbe5eab7613f229df527b2ab774 (diff)
downloadvpn_manager_bot-75e99ca0712a2c09230e5c6f8d093dc526cc717d.tar.gz
vpn_manager_bot-75e99ca0712a2c09230e5c6f8d093dc526cc717d.zip
Add users command
Diffstat (limited to 'handlers')
-rw-r--r--handlers/admin/__init__.py4
-rw-r--r--handlers/admin/add_user.py99
-rw-r--r--handlers/admin/users.py557
-rw-r--r--handlers/middleware.py24
-rw-r--r--handlers/user/info.py4
-rw-r--r--handlers/user/invoices.py16
-rw-r--r--handlers/user/pay_invoice.py5
-rw-r--r--handlers/user/payments.py12
8 files changed, 601 insertions, 120 deletions
diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py
index 4593ad7..bc603c0 100644
--- a/handlers/admin/__init__.py
+++ b/handlers/admin/__init__.py
@@ -4,7 +4,7 @@ from aiogram.filters import MagicData
4# isort: off 4# isort: off
5from . import new_announcement 5from . import new_announcement
6from . import new_invoice 6from . import new_invoice
7from . import add_user 7from . import users
8from . import payment_status 8from . import payment_status
9# isort: on 9# isort: on
10 10
@@ -15,6 +15,6 @@ router.callback_query.filter(MagicData(F.user.is_admin()))
15router.include_routers( 15router.include_routers(
16 new_announcement.router, 16 new_announcement.router,
17 new_invoice.router, 17 new_invoice.router,
18 add_user.router, 18 users.router,
19 payment_status.router, 19 payment_status.router,
20) 20)
diff --git a/handlers/admin/add_user.py b/handlers/admin/add_user.py
deleted file mode 100644
index 1d19834..0000000
--- a/handlers/admin/add_user.py
+++ /dev/null
@@ -1,99 +0,0 @@
1from datetime import UTC, datetime
2
3from aiogram import F, Router
4from aiogram.enums.button_style import ButtonStyle
5from aiogram.filters import Command
6from aiogram.fsm.context import FSMContext
7from aiogram.fsm.state import State, StatesGroup
8from aiogram.types import (
9 KeyboardButton,
10 KeyboardButtonRequestUsers,
11 Message,
12 ReplyKeyboardMarkup,
13 ReplyKeyboardRemove,
14)
15from pydantic import BaseModel
16from sqlalchemy.ext.asyncio import AsyncSession
17
18from libs.fsm import edit_data, get_data, set_data
19from models import User
20
21router = Router(name="add_user")
22
23
24class AddUserStates(StatesGroup):
25 user_id = State()
26 vpn_link = State()
27
28
29class AddUserData(BaseModel):
30 user_id: int | None = None
31
32
33CANCEL_BUTTON = "Отменить добавление"
34
35
36@router.message(Command("add_user"))
37async def command(msg: Message, state: FSMContext) -> None:
38 await msg.answer(
39 "Выберете пользователя которого хотите добавить.",
40 reply_markup=ReplyKeyboardMarkup(
41 keyboard=[
42 [
43 KeyboardButton(
44 text="Выбрать пользователя",
45 style=ButtonStyle.PRIMARY,
46 request_users=KeyboardButtonRequestUsers(request_id=0),
47 ),
48 ],
49 [
50 KeyboardButton(text=CANCEL_BUTTON, style=ButtonStyle.DANGER),
51 ],
52 ],
53 resize_keyboard=True,
54 ),
55 )
56 await set_data(state, AddUserData(user_id=None))
57 await state.set_state(AddUserStates.user_id)
58
59
60@router.message(AddUserStates(), F.text == CANCEL_BUTTON)
61async def cancel(msg: Message, state: FSMContext) -> None:
62 await msg.answer(
63 "Добавление пользователей отменено",
64 reply_markup=ReplyKeyboardRemove(),
65 )
66 await state.clear()
67
68
69@router.message(AddUserStates.user_id)
70async def set_user_id(msg: Message, state: FSMContext) -> None:
71 if msg.users_shared is None:
72 await msg.answer("Вы должны воспользоваться кнопкой ниже.")
73 return
74
75 async with edit_data(state, AddUserData) as data:
76 data.user_id = msg.users_shared.users[0].user_id
77
78 await msg.answer("Укажите ссылку для доступа к VPN")
79 await state.set_state(AddUserStates.vpn_link)
80
81
82@router.message(AddUserStates.vpn_link)
83async def set_vpn_link(
84 msg: Message,
85 state: FSMContext,
86 session: AsyncSession,
87) -> None:
88 if msg.text is None:
89 await msg.answer("Вы должны указать ссылку отправив текстовое сообщение.")
90 return
91
92 data = await get_data(state, AddUserData)
93 assert data.user_id is not None
94
95 session.add(User(id=data.user_id, vpn_link=msg.text, datetime=datetime.now(UTC)))
96 await session.flush()
97
98 await msg.answer("Пользователь добавлен.", reply_markup=ReplyKeyboardRemove())
99 await state.clear()
diff --git a/handlers/admin/users.py b/handlers/admin/users.py
new file mode 100644
index 0000000..88dfd89
--- /dev/null
+++ b/handlers/admin/users.py
@@ -0,0 +1,557 @@
1from datetime import UTC, datetime
2from math import ceil
3
4from aiogram import Bot, F, Router
5from aiogram.enums import ButtonStyle
6from aiogram.filters import Command
7from aiogram.fsm.context import FSMContext
8from aiogram.fsm.state import State, StatesGroup
9from aiogram.types import (
10 CallbackQuery,
11 InlineKeyboardButton,
12 InlineKeyboardMarkup,
13 KeyboardButton,
14 KeyboardButtonRequestUsers,
15 Message,
16 ReplyKeyboardMarkup,
17 ReplyKeyboardRemove,
18)
19from pydantic import BaseModel
20from sqlalchemy import delete, select
21from sqlalchemy.ext.asyncio import AsyncSession
22from sqlalchemy.sql.functions import count
23
24from libs.fsm import edit_data, get_data, set_data
25from libs.user import load_user_cache
26from models import Payment, User, UserRole
27from models.callback_data import (
28 UserAddClb,
29 UserDeleteClb,
30 UserDeleteConfirmClb,
31 UserItemClb,
32 UserPageClb,
33 UserRoleClb,
34 UserRoleSetClb,
35 UserVpnLinkClb,
36)
37
38router = Router(name="users")
39PAGE_SIZE = 5
40ROLE_ICON = {
41 UserRole.ADMIN: "👑",
42 UserRole.REGULAR: "👤",
43}
44ROLE_NAME = {
45 UserRole.ADMIN: "Администратор",
46 UserRole.REGULAR: "Пользователь",
47}
48
49LIST_TEXT = "Список пользователей:"
50ADD_CANCEL_BUTTON = "Отменить добавление"
51EDIT_CANCEL_BUTTON = "Отменить изменение"
52
53
54class AddUserStates(StatesGroup):
55 user_id = State()
56 vpn_link = State()
57
58
59class AddUserData(BaseModel):
60 user_id: int | None = None
61
62
63class EditVpnLinkStates(StatesGroup):
64 vpn_link = State()
65
66
67class EditVpnLinkData(BaseModel):
68 page: int
69 user_id: int
70
71
72async def get_list_markup(
73 page: int,
74 bot: Bot,
75 session: AsyncSession,
76) -> InlineKeyboardMarkup:
77 total = await session.scalar(select(count()).select_from(User))
78 assert total is not None
79 total_pages = max(1, ceil(total / PAGE_SIZE))
80 page = max(0, min(page, total_pages - 1))
81
82 users = await session.scalars(
83 select(User).offset(PAGE_SIZE * page).limit(PAGE_SIZE).order_by(User.id.desc())
84 )
85
86 user_buttons = []
87 for u in users:
88 user_cache = await load_user_cache(bot, u.id)
89 user_buttons.append(
90 [
91 InlineKeyboardButton(
92 text=f"{ROLE_ICON[u.role]} {user_cache.full_name}",
93 callback_data=UserItemClb(page=page, user_id=u.id).pack(),
94 )
95 ]
96 )
97
98 page_buttons = []
99 if page > 0:
100 page_buttons.append(
101 InlineKeyboardButton(
102 text="◀️",
103 callback_data=UserPageClb(page=page - 1).pack(),
104 )
105 )
106 if page < total_pages - 1:
107 page_buttons.append(
108 InlineKeyboardButton(
109 text="▶️",
110 callback_data=UserPageClb(page=page + 1).pack(),
111 )
112 )
113
114 add_row = [
115 InlineKeyboardButton(
116 text="➕ Добавить пользователя",
117 callback_data=UserAddClb().pack(),
118 )
119 ]
120
121 return InlineKeyboardMarkup(inline_keyboard=[*user_buttons, page_buttons, add_row])
122
123
124def build_item_markup(user_id: int, page: int) -> InlineKeyboardMarkup:
125 return InlineKeyboardMarkup(
126 inline_keyboard=[
127 [
128 InlineKeyboardButton(
129 text="Поменять роль",
130 callback_data=UserRoleClb(page=page, user_id=user_id).pack(),
131 ),
132 ],
133 [
134 InlineKeyboardButton(
135 text="Изменить VPN ссылку",
136 callback_data=UserVpnLinkClb(page=page, user_id=user_id).pack(),
137 ),
138 ],
139 [
140 InlineKeyboardButton(
141 text="Удалить пользователя",
142 style=ButtonStyle.DANGER,
143 callback_data=UserDeleteClb(page=page, user_id=user_id).pack(),
144 ),
145 ],
146 [
147 InlineKeyboardButton(
148 text="Назад к списку",
149 callback_data=UserPageClb(page=page).pack(),
150 ),
151 ],
152 ]
153 )
154
155
156def build_role_markup(user_id: int, page: int) -> InlineKeyboardMarkup:
157 return InlineKeyboardMarkup(
158 inline_keyboard=[
159 [
160 InlineKeyboardButton(
161 text=f"{ROLE_ICON[UserRole.REGULAR]} {ROLE_NAME[UserRole.REGULAR]}",
162 callback_data=UserRoleSetClb(
163 page=page,
164 user_id=user_id,
165 role=UserRole.REGULAR,
166 ).pack(),
167 ),
168 ],
169 [
170 InlineKeyboardButton(
171 text=f"{ROLE_ICON[UserRole.ADMIN]} {ROLE_NAME[UserRole.ADMIN]}",
172 callback_data=UserRoleSetClb(
173 page=page,
174 user_id=user_id,
175 role=UserRole.ADMIN,
176 ).pack(),
177 ),
178 ],
179 [
180 InlineKeyboardButton(
181 text="Назад",
182 callback_data=UserItemClb(page=page, user_id=user_id).pack(),
183 ),
184 ],
185 ]
186 )
187
188
189def build_delete_markup(user_id: int, page: int) -> InlineKeyboardMarkup:
190 return InlineKeyboardMarkup(
191 inline_keyboard=[
192 [
193 InlineKeyboardButton(
194 text="❌ Удалить",
195 style=ButtonStyle.DANGER,
196 callback_data=UserDeleteConfirmClb(
197 page=page,
198 user_id=user_id,
199 ).pack(),
200 ),
201 ],
202 [
203 InlineKeyboardButton(
204 text="Назад",
205 callback_data=UserItemClb(page=page, user_id=user_id).pack(),
206 ),
207 ],
208 ]
209 )
210
211
212async def get_item_text(bot: Bot, target: User) -> str:
213 user_cache = await load_user_cache(bot, target.id)
214 return (
215 f"Пользователь: {user_cache.mention}\n"
216 f"Роль: {ROLE_NAME[target.role]}\n"
217 f"VPN ссылка: {target.vpn_link}"
218 )
219
220
221@router.message(Command("users"))
222async def command(msg: Message, bot: Bot, session: AsyncSession) -> None:
223 await msg.answer(
224 LIST_TEXT,
225 reply_markup=await get_list_markup(0, bot, session),
226 )
227
228
229@router.callback_query(UserPageClb.filter())
230async def page(
231 clb: CallbackQuery,
232 bot: Bot,
233 callback_data: UserPageClb,
234 session: AsyncSession,
235) -> None:
236 assert isinstance(clb.message, Message)
237 await clb.message.edit_text(
238 LIST_TEXT,
239 reply_markup=await get_list_markup(callback_data.page, bot, session),
240 )
241 await clb.answer()
242
243
244@router.callback_query(UserItemClb.filter())
245async def item(
246 clb: CallbackQuery,
247 bot: Bot,
248 callback_data: UserItemClb,
249 session: AsyncSession,
250) -> None:
251 assert isinstance(clb.message, Message)
252 target = await session.get(User, callback_data.user_id)
253 if target is None:
254 await clb.answer("Пользователь не найден.", show_alert=True)
255 return
256
257 await clb.message.edit_text(
258 await get_item_text(bot, target),
259 reply_markup=build_item_markup(target.id, callback_data.page),
260 )
261 await clb.answer()
262
263
264@router.callback_query(UserRoleClb.filter())
265async def role_menu(
266 clb: CallbackQuery,
267 callback_data: UserRoleClb,
268 session: AsyncSession,
269 user: User,
270) -> None:
271 assert isinstance(clb.message, Message)
272 if user.id == callback_data.user_id:
273 await clb.answer("Нельзя поменять свою роль.", show_alert=True)
274 return
275
276 target = await session.get(User, callback_data.user_id)
277 if target is None:
278 await clb.answer("Пользователь не найден.", show_alert=True)
279 return
280
281 await clb.message.edit_text(
282 f"Выберете новую роль для пользователя (текущая: {ROLE_NAME[target.role]}):",
283 reply_markup=build_role_markup(target.id, callback_data.page),
284 )
285 await clb.answer()
286
287
288@router.callback_query(UserRoleSetClb.filter())
289async def role_set(
290 clb: CallbackQuery,
291 bot: Bot,
292 callback_data: UserRoleSetClb,
293 session: AsyncSession,
294 user: User,
295) -> None:
296 assert isinstance(clb.message, Message)
297 if user.id == callback_data.user_id:
298 await clb.answer("Нельзя поменять свою роль.", show_alert=True)
299 return
300
301 target = await session.get(User, callback_data.user_id)
302 if target is None:
303 await clb.answer("Пользователь не найден.", show_alert=True)
304 return
305
306 if target.role != callback_data.role:
307 target.role = callback_data.role
308 await session.flush()
309
310 await clb.message.edit_text(
311 await get_item_text(bot, target),
312 reply_markup=build_item_markup(target.id, callback_data.page),
313 )
314 await clb.answer(f"Роль: {ROLE_NAME[target.role]}.")
315
316
317@router.callback_query(UserDeleteClb.filter())
318async def delete_menu(
319 clb: CallbackQuery,
320 bot: Bot,
321 callback_data: UserDeleteClb,
322 session: AsyncSession,
323 user: User,
324) -> None:
325 assert isinstance(clb.message, Message)
326 if user.id == callback_data.user_id:
327 await clb.answer("Нельзя удалить себя.", show_alert=True)
328 return
329
330 target = await session.get(User, callback_data.user_id)
331 if target is None:
332 await clb.answer("Пользователь не найден.", show_alert=True)
333 return
334
335 user_cache = await load_user_cache(bot, target.id)
336 await clb.message.edit_text(
337 f"Удалить пользователя {user_cache.mention}?",
338 reply_markup=build_delete_markup(target.id, callback_data.page),
339 )
340 await clb.answer()
341
342
343@router.callback_query(UserDeleteConfirmClb.filter())
344async def delete_confirm(
345 clb: CallbackQuery,
346 bot: Bot,
347 callback_data: UserDeleteConfirmClb,
348 session: AsyncSession,
349 user: User,
350) -> None:
351 assert isinstance(clb.message, Message)
352 if user.id == callback_data.user_id:
353 await clb.answer("Нельзя удалить себя.", show_alert=True)
354 return
355
356 target = await session.get(User, callback_data.user_id)
357 if target is None:
358 await clb.answer("Пользователь не найден.", show_alert=True)
359 return
360
361 await session.execute(delete(Payment).where(Payment.user_id == target.id))
362 await session.delete(target)
363 await session.flush()
364
365 await clb.message.edit_text(
366 LIST_TEXT,
367 reply_markup=await get_list_markup(callback_data.page, bot, session),
368 )
369 await clb.answer("Пользователь удалён.")
370
371
372@router.callback_query(UserVpnLinkClb.filter())
373async def vpn_link_button(
374 clb: CallbackQuery,
375 bot: Bot,
376 state: FSMContext,
377 callback_data: UserVpnLinkClb,
378 session: AsyncSession,
379) -> None:
380 assert isinstance(clb.message, Message)
381 target = await session.get(User, callback_data.user_id)
382 if target is None:
383 await clb.answer("Пользователь не найден.", show_alert=True)
384 return
385
386 await clb.message.delete()
387 await bot.send_message(
388 clb.from_user.id,
389 "Укажите новую ссылку для доступа к VPN.",
390 reply_markup=ReplyKeyboardMarkup(
391 keyboard=[
392 [KeyboardButton(text=EDIT_CANCEL_BUTTON, style=ButtonStyle.DANGER)]
393 ],
394 resize_keyboard=True,
395 ),
396 )
397 await set_data(
398 state,
399 EditVpnLinkData(page=callback_data.page, user_id=callback_data.user_id),
400 )
401 await state.set_state(EditVpnLinkStates.vpn_link)
402 await clb.answer()
403
404
405async def send_item(bot: Bot, chat_id: int, target: User, page: int) -> None:
406 await bot.send_message(
407 chat_id,
408 await get_item_text(bot, target),
409 reply_markup=build_item_markup(target.id, page),
410 )
411
412
413@router.message(EditVpnLinkStates.vpn_link, F.text == EDIT_CANCEL_BUTTON)
414async def vpn_link_cancel(
415 msg: Message,
416 bot: Bot,
417 state: FSMContext,
418 session: AsyncSession,
419) -> None:
420 data = await get_data(state, EditVpnLinkData)
421 await state.clear()
422
423 await msg.answer(
424 "Изменение VPN ссылки отменено.",
425 reply_markup=ReplyKeyboardRemove(),
426 )
427
428 target = await session.get(User, data.user_id)
429 if target is None:
430 return
431 await send_item(bot, msg.chat.id, target, data.page)
432
433
434@router.message(EditVpnLinkStates.vpn_link)
435async def vpn_link_set(
436 msg: Message,
437 bot: Bot,
438 state: FSMContext,
439 session: AsyncSession,
440) -> None:
441 if msg.text is None:
442 await msg.answer("Вы должны указать ссылку отправив текстовое сообщение.")
443 return
444
445 data = await get_data(state, EditVpnLinkData)
446 target = await session.get(User, data.user_id)
447 if target is None:
448 await msg.answer(
449 "Пользователь не найден.",
450 reply_markup=ReplyKeyboardRemove(),
451 )
452 await state.clear()
453 return
454
455 target.vpn_link = msg.text
456 await session.flush()
457 await state.clear()
458
459 await msg.answer("VPN ссылка обновлена.", reply_markup=ReplyKeyboardRemove())
460 await send_item(bot, msg.chat.id, target, data.page)
461
462
463@router.callback_query(UserAddClb.filter())
464async def add_button(
465 clb: CallbackQuery,
466 bot: Bot,
467 state: FSMContext,
468) -> None:
469 assert isinstance(clb.message, Message)
470 await clb.message.delete()
471 await bot.send_message(
472 clb.from_user.id,
473 "Выберете пользователя которого хотите добавить.",
474 reply_markup=ReplyKeyboardMarkup(
475 keyboard=[
476 [
477 KeyboardButton(
478 text="Выбрать пользователя",
479 style=ButtonStyle.PRIMARY,
480 request_users=KeyboardButtonRequestUsers(request_id=0),
481 ),
482 ],
483 [KeyboardButton(text=ADD_CANCEL_BUTTON, style=ButtonStyle.DANGER)],
484 ],
485 resize_keyboard=True,
486 ),
487 )
488 await set_data(state, AddUserData(user_id=None))
489 await state.set_state(AddUserStates.user_id)
490 await clb.answer()
491
492
493@router.message(AddUserStates(), F.text == ADD_CANCEL_BUTTON)
494async def add_cancel(
495 msg: Message,
496 bot: Bot,
497 state: FSMContext,
498 session: AsyncSession,
499) -> None:
500 await msg.answer(
501 "Добавление пользователя отменено.",
502 reply_markup=ReplyKeyboardRemove(),
503 )
504
505 await msg.answer(
506 LIST_TEXT,
507 reply_markup=await get_list_markup(0, bot, session),
508 )
509 await state.clear()
510
511
512@router.message(AddUserStates.user_id)
513async def add_set_user_id(msg: Message, state: FSMContext) -> None:
514 if msg.users_shared is None:
515 await msg.answer("Вы должны воспользоваться кнопкой ниже.")
516 return
517
518 async with edit_data(state, AddUserData) as data:
519 data.user_id = msg.users_shared.users[0].user_id
520
521 await msg.answer(
522 "Укажите ссылку для доступа к VPN.",
523 reply_markup=ReplyKeyboardMarkup(
524 keyboard=[
525 [KeyboardButton(text=ADD_CANCEL_BUTTON, style=ButtonStyle.DANGER)]
526 ],
527 resize_keyboard=True,
528 ),
529 )
530 await state.set_state(AddUserStates.vpn_link)
531
532
533@router.message(AddUserStates.vpn_link)
534async def add_set_vpn_link(
535 msg: Message,
536 bot: Bot,
537 state: FSMContext,
538 session: AsyncSession,
539) -> None:
540 if msg.text is None:
541 await msg.answer("Вы должны указать ссылку отправив текстовое сообщение.")
542 return
543
544 data = await get_data(state, AddUserData)
545 assert data.user_id is not None
546
547 session.add(User(id=data.user_id, vpn_link=msg.text, datetime=datetime.now(UTC)))
548 await session.flush()
549
550 await msg.answer("Пользователь добавлен.", reply_markup=ReplyKeyboardRemove())
551
552 target = await session.get(User, data.user_id)
553 if target is None:
554 return
555
556 await send_item(bot, msg.chat.id, target, 0)
557 await state.clear()
diff --git a/handlers/middleware.py b/handlers/middleware.py
index 87a117a..376f4a2 100644
--- a/handlers/middleware.py
+++ b/handlers/middleware.py
@@ -5,7 +5,9 @@ from aiogram.types import CallbackQuery, Message, TelegramObject
5from sqlalchemy.ext.asyncio import AsyncSession 5from sqlalchemy.ext.asyncio import AsyncSession
6 6
7from database import sessions 7from database import sessions
8from libs.user import set_user_cache
8from models import User 9from models import User
10from models.user import UserCache
9 11
10 12
11class InjectSessionMiddleware(BaseMiddleware): 13class InjectSessionMiddleware(BaseMiddleware):
@@ -54,3 +56,25 @@ class UserAccessMiddleware(BaseMiddleware):
54 data["user"] = user 56 data["user"] = user
55 57
56 return await handler(event, data) 58 return await handler(event, data)
59
60
61class UserCacheMiddleware(BaseMiddleware):
62 async def __call__(
63 self,
64 handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
65 event: TelegramObject,
66 data: dict[str, Any],
67 ) -> Any:
68 if not isinstance(event, (Message, CallbackQuery)):
69 raise TypeError(
70 f"UserAccessMiddleware doesn't support event with type: {type(event).__name__}",
71 event,
72 )
73
74 if isinstance(event, Message):
75 user_cache = UserCache.from_chat(event.chat)
76 else:
77 user_cache = UserCache.from_user(event.from_user)
78
79 await set_user_cache(user_cache)
80 return await handler(event, data)
diff --git a/handlers/user/info.py b/handlers/user/info.py
index 9776e7e..587925f 100644
--- a/handlers/user/info.py
+++ b/handlers/user/info.py
@@ -47,8 +47,8 @@ ADMIN_COMMANDS = COMMANDS + [
47 description="Создать новый счёт на оплату", 47 description="Создать новый счёт на оплату",
48 ), 48 ),
49 BotCommand( 49 BotCommand(
50 command="add_user", 50 command="users",
51 description="дть пользователя", 51 description="ра пользователями",
52 ), 52 ),
53 BotCommand( 53 BotCommand(
54 command="suggest_list", 54 command="suggest_list",
diff --git a/handlers/user/invoices.py b/handlers/user/invoices.py
index cc071bb..15785fb 100644
--- a/handlers/user/invoices.py
+++ b/handlers/user/invoices.py
@@ -18,7 +18,7 @@ from libs.invoice import (
18 get_payment_status, 18 get_payment_status,
19) 19)
20from libs.msg import eclipse_text 20from libs.msg import eclipse_text
21from libs.user import mention 21from libs.user import load_user_cache
22from models import Invoice, PaymentStatus, User 22from models import Invoice, PaymentStatus, User
23from models.callback_data import InvoiceItemClb, InvoicePageClb, PayInvoiceClb 23from models.callback_data import InvoiceItemClb, InvoicePageClb, PayInvoiceClb
24 24
@@ -166,14 +166,14 @@ async def item(
166 ] 166 ]
167 ) 167 )
168 168
169 await clb.message.edit_text(text_template.format("..."), reply_markup=reply_markup)
170 user_status = [] 169 user_status = []
171 for user_id, s in invoice_payments.user_status.items(): 170 for user_id, s in invoice_payments.user_status.items():
172 chat = await bot.get_chat(user_id) 171 user_cache = await load_user_cache(bot, user_id)
173 user_status.append(f"{PAYMENT_STATUS[s]} - {mention(chat)}") 172 user_status.append(f"{PAYMENT_STATUS[s]} - {user_cache.mention}")
174 await clb.message.edit_text( 173
175 text_template.format("\n".join(user_status)), 174 await clb.message.edit_text(
176 reply_markup=reply_markup, 175 text_template.format("\n".join(user_status)),
177 ) 176 reply_markup=reply_markup,
177 )
178 178
179 await clb.answer() 179 await clb.answer()
diff --git a/handlers/user/pay_invoice.py b/handlers/user/pay_invoice.py
index db75f47..d7809cd 100644
--- a/handlers/user/pay_invoice.py
+++ b/handlers/user/pay_invoice.py
@@ -19,7 +19,6 @@ from sqlalchemy import and_, select
19from sqlalchemy.ext.asyncio import AsyncSession 19from sqlalchemy.ext.asyncio import AsyncSession
20 20
21from libs.fsm import get_data, set_data 21from libs.fsm import get_data, set_data
22from libs.user import mention
23from models import ( 22from models import (
24 Invoice, 23 Invoice,
25 Payment, 24 Payment,
@@ -27,6 +26,7 @@ from models import (
27 ReceiptFile, 26 ReceiptFile,
28 ReceiptFileType, 27 ReceiptFileType,
29 User, 28 User,
29 UserCache,
30 UserRole, 30 UserRole,
31) 31)
32from models.callback_data import PayInvoiceClb, PaymentStatusClb 32from models.callback_data import PayInvoiceClb, PaymentStatusClb
@@ -112,6 +112,7 @@ async def receipt(
112 bot: Bot, 112 bot: Bot,
113 state: FSMContext, 113 state: FSMContext,
114 session: AsyncSession, 114 session: AsyncSession,
115 user_cache: UserCache,
115) -> None: 116) -> None:
116 if msg.document is not None: 117 if msg.document is not None:
117 receipt_file = ReceiptFile( 118 receipt_file = ReceiptFile(
@@ -173,7 +174,7 @@ async def receipt(
173 try: 174 try:
174 await bot.send_message( 175 await bot.send_message(
175 admin_id, 176 admin_id,
176 f"Новое подтверждение оплаты:\nПользователь: {mention(msg.chat)}", 177 f"Новое подтверждение оплаты:\nПользователь: {user_cache.mention}",
177 ) 178 )
178 await receipt_file.send(bot, admin_id, reply_markup=reply_markup) 179 await receipt_file.send(bot, admin_id, reply_markup=reply_markup)
179 except TelegramAPIError as e: 180 except TelegramAPIError as e:
diff --git a/handlers/user/payments.py b/handlers/user/payments.py
index 87ea236..e15a882 100644
--- a/handlers/user/payments.py
+++ b/handlers/user/payments.py
@@ -15,13 +15,13 @@ from sqlalchemy.sql.functions import count
15 15
16from libs.invoice import get_payment_status 16from libs.invoice import get_payment_status
17from libs.msg import eclipse_text 17from libs.msg import eclipse_text
18from libs.user import mention 18from libs.user import load_user_cache
19from models import Invoice, Payment, PaymentStatus, User 19from models import Invoice, Payment, PaymentStatus, User
20from models.callback_data import ( 20from models.callback_data import (
21 PayInvoiceClb,
21 PaymentItemClb, 22 PaymentItemClb,
22 PaymentPageClb, 23 PaymentPageClb,
23 PaymentStatusClb, 24 PaymentStatusClb,
24 PayInvoiceClb,
25) 25)
26 26
27router = Router(name="payments") 27router = Router(name="payments")
@@ -213,7 +213,7 @@ async def item(
213 213
214 invoice = await session.get(Invoice, payment.invoice_id) 214 invoice = await session.get(Invoice, payment.invoice_id)
215 assert invoice is not None 215 assert invoice is not None
216 chat = await bot.get_chat(payment.user_id) 216 user_cache = await load_user_cache(bot, payment.user_id)
217 217
218 status_buttons = [] 218 status_buttons = []
219 if payment.status != PaymentStatus.ACCEPTED: 219 if payment.status != PaymentStatus.ACCEPTED:
@@ -242,12 +242,10 @@ async def item(
242 text="Назад к выбору", 242 text="Назад к выбору",
243 callback_data=PaymentPageClb(page=callback_data.page).pack(), 243 callback_data=PaymentPageClb(page=callback_data.page).pack(),
244 ) 244 )
245 reply_markup = InlineKeyboardMarkup( 245 reply_markup = InlineKeyboardMarkup(inline_keyboard=[status_buttons, [back_button]])
246 inline_keyboard=[status_buttons, [back_button]]
247 )
248 246
249 caption = ( 247 caption = (
250 f"Платёж от {mention(chat)}\n" 248 f"Платёж от {user_cache.mention}\n"
251 f"Счёт: {eclipse_text(invoice.message.text, 30)}\n" 249 f"Счёт: {eclipse_text(invoice.message.text, 30)}\n"
252 f"Дата: {payment.datetime.strftime('%d %b %y г.')}\n" 250 f"Дата: {payment.datetime.strftime('%d %b %y г.')}\n"
253 f"Статус: {PAYMENT_STATUS[payment.status]}" 251 f"Статус: {PAYMENT_STATUS[payment.status]}"