aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--handlers/admin/new_invoice.py4
-rw-r--r--handlers/admin/payment_status.py8
-rw-r--r--handlers/user/__init__.py2
-rw-r--r--handlers/user/announcements.py21
-rw-r--r--handlers/user/invoices.py179
-rw-r--r--handlers/user/pay_invoice.py22
-rw-r--r--libs/__init__.py3
-rw-r--r--libs/invoice.py69
-rw-r--r--main.py2
-rw-r--r--models/callback_data.py9
10 files changed, 307 insertions, 12 deletions
diff --git a/handlers/admin/new_invoice.py b/handlers/admin/new_invoice.py
index 43e47cb..f532bff 100644
--- a/handlers/admin/new_invoice.py
+++ b/handlers/admin/new_invoice.py
@@ -18,7 +18,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
18 18
19from libs.fsm import get_data, set_data 19from libs.fsm import get_data, set_data
20from libs.msg import send_invoice 20from libs.msg import send_invoice
21from models import Invoice, RichText, User 21from models import Invoice, RichText, User, UserRole
22 22
23router = Router(name="new_invoice") 23router = Router(name="new_invoice")
24 24
@@ -59,7 +59,7 @@ async def create(
59 state: FSMContext, 59 state: FSMContext,
60 session: AsyncSession, 60 session: AsyncSession,
61) -> None: 61) -> None:
62 users = await session.scalars(select(User.id).where(User.id != msg.chat.id)) 62 users = await session.scalars(select(User.id).where(User.role != UserRole.ADMIN))
63 data = await get_data(state, NewInvoiceData) 63 data = await get_data(state, NewInvoiceData)
64 64
65 if data.rich_text is None: 65 if data.rich_text is None:
diff --git a/handlers/admin/payment_status.py b/handlers/admin/payment_status.py
index 23bde15..8784ac4 100644
--- a/handlers/admin/payment_status.py
+++ b/handlers/admin/payment_status.py
@@ -11,14 +11,18 @@ router = Router(name="payment_status")
11 11
12 12
13async def accept(clb: CallbackQuery, bot: Bot, payment: Payment) -> None: 13async def accept(clb: CallbackQuery, bot: Bot, payment: Payment) -> None:
14 assert clb.message is not None
15
14 payment.status = PaymentStatus.ACCEPTED 16 payment.status = PaymentStatus.ACCEPTED
15 await bot.send_message(clb.from_user.id, "Оплата пользователя подтверждена.") 17 await clb.message.reply("Оплата пользователя подтверждена.")
16 await bot.send_message(payment.user_id, "Файл подтверждения оплаты принят.") 18 await bot.send_message(payment.user_id, "Файл подтверждения оплаты принят.")
17 19
18 20
19async def reject(clb: CallbackQuery, bot: Bot, payment: Payment) -> None: 21async def reject(clb: CallbackQuery, bot: Bot, payment: Payment) -> None:
22 assert clb.message is not None
23
20 payment.status = PaymentStatus.REJECTED 24 payment.status = PaymentStatus.REJECTED
21 await bot.send_message(clb.from_user.id, "Оплата пользователя отклонена.") 25 await clb.message.reply("Оплата пользователя отклонена.")
22 26
23 callback_data = PayInvoiceClb(invoice_id=payment.invoice_id).pack() 27 callback_data = PayInvoiceClb(invoice_id=payment.invoice_id).pack()
24 await bot.send_message( 28 await bot.send_message(
diff --git a/handlers/user/__init__.py b/handlers/user/__init__.py
index a0e719f..7eee69a 100644
--- a/handlers/user/__init__.py
+++ b/handlers/user/__init__.py
@@ -5,6 +5,7 @@ from . import info
5from . import vpn_link 5from . import vpn_link
6from . import pay_invoice 6from . import pay_invoice
7from . import announcements 7from . import announcements
8from . import invoices
8# isort: on 9# isort: on
9 10
10router = Router(name="user") 11router = Router(name="user")
@@ -13,4 +14,5 @@ router.include_routers(
13 vpn_link.router, 14 vpn_link.router,
14 pay_invoice.router, 15 pay_invoice.router,
15 announcements.router, 16 announcements.router,
17 invoices.router,
16) 18)
diff --git a/handlers/user/announcements.py b/handlers/user/announcements.py
index 8f4aa43..dc85b54 100644
--- a/handlers/user/announcements.py
+++ b/handlers/user/announcements.py
@@ -16,11 +16,17 @@ router = Router(name="announcements")
16PAGE_SIZE = 5 16PAGE_SIZE = 5
17 17
18 18
19async def get_reply_markup(page: int, session: AsyncSession) -> InlineKeyboardMarkup: 19async def get_reply_markup(
20 page: int,
21 session: AsyncSession,
22) -> InlineKeyboardMarkup | None:
20 total = await session.scalar(select(count()).select_from(Announcement)) 23 total = await session.scalar(select(count()).select_from(Announcement))
21 assert total is not None 24 assert total is not None
22 total_pages = ceil(total / PAGE_SIZE) 25 total_pages = ceil(total / PAGE_SIZE)
23 26
27 if total == 0:
28 return None
29
24 page = max(0, min(page, total_pages - 1)) 30 page = max(0, min(page, total_pages - 1))
25 query = ( 31 query = (
26 select(Announcement) 32 select(Announcement)
@@ -64,10 +70,13 @@ async def get_reply_markup(page: int, session: AsyncSession) -> InlineKeyboardMa
64 70
65@router.message(Command("announcements")) 71@router.message(Command("announcements"))
66async def command(msg: Message, bot: Bot, session: AsyncSession) -> None: 72async def command(msg: Message, bot: Bot, session: AsyncSession) -> None:
67 await msg.answer( 73 reply_markup = await get_reply_markup(0, session)
68 "Выберете анонс для просмотра.", 74
69 reply_markup=await get_reply_markup(0, session), 75 if reply_markup is None:
70 ) 76 await msg.answer("Нету анонсов для просмотра.")
77 return
78
79 await msg.answer("Выберете анонс для просмотра.", reply_markup=reply_markup)
71 80
72 81
73@router.callback_query(AnnouncePageClb.filter()) 82@router.callback_query(AnnouncePageClb.filter())
@@ -80,7 +89,7 @@ async def page(
80 89
81 reply_markup = await get_reply_markup(callback_data.page, session) 90 reply_markup = await get_reply_markup(callback_data.page, session)
82 await clb.message.edit_text( 91 await clb.message.edit_text(
83 "Выберете анонс для просмотра.", 92 "Выберете анонс для просмотра:",
84 reply_markup=reply_markup, 93 reply_markup=reply_markup,
85 ) 94 )
86 95
diff --git a/handlers/user/invoices.py b/handlers/user/invoices.py
new file mode 100644
index 0000000..cc071bb
--- /dev/null
+++ b/handlers/user/invoices.py
@@ -0,0 +1,179 @@
1from math import ceil
2
3from aiogram import Bot, Router
4from aiogram.filters import Command
5from aiogram.types import (
6 CallbackQuery,
7 InlineKeyboardButton,
8 InlineKeyboardMarkup,
9 Message,
10)
11from sqlalchemy import select
12from sqlalchemy.ext.asyncio import AsyncSession
13from sqlalchemy.sql.functions import count
14
15from libs.invoice import (
16 InvoiceStatus,
17 get_invoice_payments,
18 get_payment_status,
19)
20from libs.msg import eclipse_text
21from libs.user import mention
22from models import Invoice, PaymentStatus, User
23from models.callback_data import InvoiceItemClb, InvoicePageClb, PayInvoiceClb
24
25router = Router(name="invoices")
26PAGE_SIZE = 5
27PAYMENT_STATUS = {
28 PaymentStatus.PENDING: "🟡",
29 PaymentStatus.ACCEPTED: "🟢",
30 PaymentStatus.REJECTED: "🔴",
31}
32INVOICE_STATUS = {
33 InvoiceStatus.PAID: "🟢",
34 InvoiceStatus.UNPAID: "🔴",
35}
36
37
38def get_text(user: User) -> str:
39 if user.is_admin():
40 return "Выберете счёт для просмотра информации:"
41 else:
42 return "Выберете счёт для оплаты:"
43
44
45async def get_reply_markup(
46 page: int,
47 user: User,
48 session: AsyncSession,
49) -> InlineKeyboardMarkup | None:
50 total = await session.scalar(
51 select(count()).select_from(Invoice).where(Invoice.datetime >= user.datetime)
52 )
53 assert total is not None
54 total_pages = ceil(total / PAGE_SIZE)
55
56 if total == 0:
57 return None
58
59 page = max(0, min(page, total_pages - 1))
60 query = (
61 select(Invoice)
62 .where(Invoice.datetime >= user.datetime)
63 .offset(PAGE_SIZE * page)
64 .limit(PAGE_SIZE)
65 .order_by(Invoice.id.desc())
66 )
67 invoices = await session.scalars(query)
68
69 invoice_buttons = []
70 for i in invoices:
71 if user.is_admin():
72 invoice_payments = await get_invoice_payments(session, i)
73 status = INVOICE_STATUS[invoice_payments.status]
74 callback_data = InvoiceItemClb(page=page, invoice_id=i.id).pack()
75 else:
76 status = PAYMENT_STATUS[await get_payment_status(session, i.id, user.id)]
77 callback_data = PayInvoiceClb(invoice_id=i.id).pack()
78
79 button = InlineKeyboardButton(
80 text=(
81 f"{status} "
82 f"{eclipse_text(i.message.text, 10)} "
83 f"({i.datetime.strftime('%d %b %y г.')})"
84 ),
85 callback_data=callback_data,
86 )
87
88 invoice_buttons.append([button])
89
90 page_buttons = []
91 if page > 0:
92 page_buttons.append(
93 InlineKeyboardButton(
94 text="◀️",
95 callback_data=InvoicePageClb(page=page - 1).pack(),
96 )
97 )
98 if page < total_pages - 1:
99 page_buttons.append(
100 InlineKeyboardButton(
101 text="▶️",
102 callback_data=InvoicePageClb(page=page + 1).pack(),
103 )
104 )
105
106 return InlineKeyboardMarkup(inline_keyboard=[*invoice_buttons, page_buttons])
107
108
109@router.message(Command("invoices"))
110async def command(msg: Message, session: AsyncSession, user: User) -> None:
111 reply_markup = await get_reply_markup(0, user, session)
112
113 if reply_markup is None:
114 await msg.answer("Нету счетов для оплаты.")
115 return
116
117 await msg.answer(get_text(user), reply_markup=reply_markup)
118
119
120@router.callback_query(InvoicePageClb.filter())
121async def page(
122 clb: CallbackQuery,
123 callback_data: InvoicePageClb,
124 session: AsyncSession,
125 user: User,
126) -> None:
127 assert isinstance(clb.message, Message)
128 await clb.message.edit_text(
129 get_text(user),
130 reply_markup=await get_reply_markup(callback_data.page, user, session),
131 )
132
133 await clb.answer()
134
135
136@router.callback_query(InvoiceItemClb.filter())
137async def item(
138 clb: CallbackQuery,
139 bot: Bot,
140 callback_data: InvoiceItemClb,
141 session: AsyncSession,
142 user: User,
143) -> None:
144 assert isinstance(clb.message, Message)
145 if not user.is_admin():
146 await clb.answer("У вас нет прав для данного действия.", show_alert=True)
147 return
148
149 invoice = await session.get(Invoice, callback_data.invoice_id)
150 assert invoice is not None
151 invoice_payments = await get_invoice_payments(session, invoice)
152
153 text_template = (
154 f"Статус оплаты счёта: {INVOICE_STATUS[invoice_payments.status]}\n"
155 "Пользователи оплатившие счёт:\n"
156 "{}"
157 )
158 reply_markup = InlineKeyboardMarkup(
159 inline_keyboard=[
160 [
161 InlineKeyboardButton(
162 text="Назад к выбору",
163 callback_data=InvoicePageClb(page=callback_data.page).pack(),
164 )
165 ]
166 ]
167 )
168
169 await clb.message.edit_text(text_template.format("..."), reply_markup=reply_markup)
170 user_status = []
171 for user_id, s in invoice_payments.user_status.items():
172 chat = await bot.get_chat(user_id)
173 user_status.append(f"{PAYMENT_STATUS[s]} - {mention(chat)}")
174 await clb.message.edit_text(
175 text_template.format("\n".join(user_status)),
176 reply_markup=reply_markup,
177 )
178
179 await clb.answer()
diff --git a/handlers/user/pay_invoice.py b/handlers/user/pay_invoice.py
index 98f80a6..db75f47 100644
--- a/handlers/user/pay_invoice.py
+++ b/handlers/user/pay_invoice.py
@@ -20,7 +20,15 @@ from 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 22from libs.user import mention
23from models import Payment, PaymentStatus, ReceiptFile, ReceiptFileType, User, UserRole 23from models import (
24 Invoice,
25 Payment,
26 PaymentStatus,
27 ReceiptFile,
28 ReceiptFileType,
29 User,
30 UserRole,
31)
24from models.callback_data import PayInvoiceClb, PaymentStatusClb 32from models.callback_data import PayInvoiceClb, PaymentStatusClb
25 33
26router = Router(name="pay_invoice") 34router = Router(name="pay_invoice")
@@ -44,7 +52,19 @@ async def button(
44 state: FSMContext, 52 state: FSMContext,
45 callback_data: PayInvoiceClb, 53 callback_data: PayInvoiceClb,
46 session: AsyncSession, 54 session: AsyncSession,
55 user: User,
47) -> None: 56) -> None:
57 if user.is_admin():
58 await clb.answer("Администраторы не могут оплачивать счета", show_alert=True)
59 return
60
61 invoice = await session.get(Invoice, callback_data.invoice_id)
62 assert invoice is not None
63
64 if user.datetime > invoice.datetime:
65 await clb.answer("Вы не можете оплатить данный счёт", show_alert=True)
66 return
67
48 payment = await session.scalar( 68 payment = await session.scalar(
49 select(Payment).where( 69 select(Payment).where(
50 and_( 70 and_(
diff --git a/libs/__init__.py b/libs/__init__.py
index 65f8ab7..55e6b19 100644
--- a/libs/__init__.py
+++ b/libs/__init__.py
@@ -1,8 +1,9 @@
1from . import fsm, msg, storage, user 1from . import fsm, invoice, msg, storage, user
2 2
3__all__ = [ 3__all__ = [
4 "storage", 4 "storage",
5 "fsm", 5 "fsm",
6 "msg", 6 "msg",
7 "user", 7 "user",
8 "invoice",
8] 9]
diff --git a/libs/invoice.py b/libs/invoice.py
new file mode 100644
index 0000000..92b82f7
--- /dev/null
+++ b/libs/invoice.py
@@ -0,0 +1,69 @@
1from enum import StrEnum, auto
2
3from pydantic import BaseModel
4from sqlalchemy import and_, select
5from sqlalchemy.ext.asyncio import AsyncSession
6
7from models import Invoice, Payment, PaymentStatus, User, UserRole
8
9
10class InvoiceStatus(StrEnum):
11 PAID = auto()
12 UNPAID = auto()
13
14
15class InvoicePayments(BaseModel):
16 user_status: dict[int, PaymentStatus]
17 status: InvoiceStatus
18
19
20async def get_invoice_payments(
21 session: AsyncSession,
22 invoice: Invoice,
23) -> InvoicePayments:
24 users = await session.scalars(
25 select(User).where(
26 and_(
27 User.role != UserRole.ADMIN,
28 User.datetime <= invoice.datetime,
29 )
30 )
31 )
32 payments = await session.scalars(
33 select(Payment).where(Payment.invoice_id == invoice.id)
34 )
35
36 user_status = {u.id: PaymentStatus.REJECTED for u in users}
37 for p in payments:
38 if (
39 p.status != PaymentStatus.REJECTED
40 and user_status[p.user_id] == PaymentStatus.REJECTED
41 ):
42 user_status[p.user_id] = p.status
43
44 status = (
45 InvoiceStatus.PAID
46 if all(s == PaymentStatus.ACCEPTED for s in user_status.values())
47 else InvoiceStatus.UNPAID
48 )
49
50 return InvoicePayments(user_status=user_status, status=status)
51
52
53async def get_payment_status(
54 session: AsyncSession,
55 invoice_id: int,
56 user_id: int,
57) -> PaymentStatus:
58 payments = await session.scalars(
59 select(Payment).where(
60 and_(
61 Payment.invoice_id == invoice_id,
62 Payment.user_id == user_id,
63 )
64 )
65 )
66 for p in payments:
67 if p.status != PaymentStatus.REJECTED:
68 return p.status
69 return PaymentStatus.REJECTED
diff --git a/main.py b/main.py
index 322e771..bb9c7ea 100644
--- a/main.py
+++ b/main.py
@@ -1,4 +1,5 @@
1import asyncio 1import asyncio
2import locale
2import logging 3import logging
3 4
4from aiogram.enums import UpdateType 5from aiogram.enums import UpdateType
@@ -6,6 +7,7 @@ from aiogram.enums import UpdateType
6from handlers import router 7from handlers import router
7from shared import bot, dp 8from shared import bot, dp
8 9
10locale.setlocale(locale.LC_ALL, "ru_RU.UTF-8")
9logging.basicConfig(level=logging.INFO) 11logging.basicConfig(level=logging.INFO)
10 12
11dp.include_router(router) 13dp.include_router(router)
diff --git a/models/callback_data.py b/models/callback_data.py
index 4d37226..f9c9a26 100644
--- a/models/callback_data.py
+++ b/models/callback_data.py
@@ -19,3 +19,12 @@ class AnnouncePageClb(CallbackData, prefix="a10t.p"):
19class AnnounceItemClb(CallbackData, prefix="a10t.i"): 19class AnnounceItemClb(CallbackData, prefix="a10t.i"):
20 page: int 20 page: int
21 announce_id: int 21 announce_id: int
22
23
24class InvoicePageClb(CallbackData, prefix="invoice.p"):
25 page: int
26
27
28class InvoiceItemClb(CallbackData, prefix="invoice.i"):
29 page: int
30 invoice_id: int