diff --git a/.gitignore b/.gitignore index d2882aa..0797a9d 100644 --- a/.gitignore +++ b/.gitignore @@ -177,4 +177,5 @@ cython_debug/ # Static / IMG static/img -static/* \ No newline at end of file +static/* +.env.prod \ No newline at end of file diff --git a/config.py b/config.py index 442bf31..b3d56ee 100644 --- a/config.py +++ b/config.py @@ -29,6 +29,11 @@ REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/1") PROXY_URL = os.getenv("PROXY_URL") WELCOME_VIDEO_NOTE_FP = os.getenv("WELCOME_VIDEO_NOTE_FP", "static/welcome.mp4") +CHANNEL_URL = os.getenv("CHANNEL_URL", "https://t.me/shveitech") +CHANNEL_ID = os.getenv("CHANNEL_ID") + +if not CHANNEL_ID: + raise Exception("Specify Channel ID in .env") ### Constants ### diff --git a/handlers/client/checkout.py b/handlers/client/checkout.py index 8068109..3634450 100644 --- a/handlers/client/checkout.py +++ b/handlers/client/checkout.py @@ -5,15 +5,15 @@ from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message from sqlalchemy.ext.asyncio import AsyncSession -from config import NOTIFICATION_CHANNEL +from config import CHANNEL_ID, NOTIFICATION_CHANNEL from db.models.orders import OrderStatus from dto.checkout import CheckoutContext from misc.kb.admins import customer_contacts -from misc.kb.client import order_confirmation, order_specs_confirmation +from misc.kb.client import order_confirmation, order_specs_confirmation, subscription from misc.kb.common import return_menu from misc.states import CheckoutStorage from misc.texts import get_order_item_list -from misc.utils import is_valid_phone +from misc.utils import is_member, is_valid_phone from repositories.orders import OrderRepository from services.orders import OrderService @@ -180,3 +180,30 @@ async def payment_create( ) logger.exception(e) return + + +@router.callback_query(F.data.startswith("precheck_order:")) +async def precheck_order( + cb: CallbackQuery, state: FSMContext, session: AsyncSession, order_repo: OrderRepository +): + await state.clear() + order_id = cb.data.split(":")[1] + if not order_id.isdigit(): + await cb.message.edit_text("🍃 Что-то пошло не так.", reply_markup=return_menu) + return + + member = await cb.bot.get_chat_member(CHANNEL_ID, cb.from_user.id) + + if not is_member(member): + await cb.message.edit_text( + "📚 Мы заметили, что вы не подписаны на канал.", + reply_markup=subscription(order_id), + ) + return + + await order_confirm( + cb=cb, + state=state, + session=session, + order_repo=order_repo, + ) diff --git a/misc/kb/client.py b/misc/kb/client.py index c592377..bcffbb9 100644 --- a/misc/kb/client.py +++ b/misc/kb/client.py @@ -3,6 +3,7 @@ from collections.abc import Callable from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder +import config from config import PAGE_SIZE from db.models import Category, Product from dto.cart import CartItemDTO @@ -81,6 +82,7 @@ def main_menu_kb(cart_items: int) -> InlineKeyboardMarkup: InlineKeyboardButton(text="🔍 Поиск", callback_data="find"), ], [cart_btn(cart_items)], + [InlineKeyboardButton(text="🌸 Подписаться на канал", url=config.CHANNEL_URL)], ] ).as_markup() @@ -168,9 +170,7 @@ def render_products( nav_buttons = [] if prev_cb: nav_buttons.append(InlineKeyboardButton(text="◀️", callback_data=prev_cb)) - page_text = ( - f"{page + 1}/{total_pages}" if total_pages is not None else f"{page + 1}" - ) + page_text = f"{page + 1}/{total_pages}" if total_pages is not None else f"{page + 1}" nav_buttons.append(InlineKeyboardButton(text=page_text, callback_data="...")) if next_cb: nav_buttons.append(InlineKeyboardButton(text="▶️", callback_data=next_cb)) @@ -298,7 +298,7 @@ def order_confirmation(order_id: int) -> InlineKeyboardMarkup: [ InlineKeyboardButton( text="🟢 Подтвердить заказ", - callback_data=f"confirm_order:{order_id}", + callback_data=f"precheck_order:{order_id}", ) ], [InlineKeyboardButton(text="❌ Отменить", callback_data="menu:main")], @@ -306,6 +306,22 @@ def order_confirmation(order_id: int) -> InlineKeyboardMarkup: ).as_markup() +def subscription(order_id: int) -> InlineKeyboardMarkup: + return InlineKeyboardBuilder( + [ + [InlineKeyboardButton(text="🌸 Подписаться на канал", url=config.CHANNEL_URL)], + [ + InlineKeyboardButton( + text="▶️ Продолжить оформление", callback_data=f"confirm_order:{order_id}" + ) + ], + [ + InlineKeyboardButton(text="🏠", callback_data="menu:main") + ] + ] + ).as_markup() + + def order_specs_confirmation(order_id: int) -> InlineKeyboardMarkup: return InlineKeyboardBuilder( [ diff --git a/misc/utils.py b/misc/utils.py index 064a66b..7685f04 100644 --- a/misc/utils.py +++ b/misc/utils.py @@ -3,6 +3,12 @@ import json from typing import Any import phonenumbers +from aiogram.types import ( + ChatMemberAdministrator, + ChatMemberMember, + ChatMemberOwner, + ResultChatMemberUnion, +) from phonenumbers import NumberParseException @@ -26,3 +32,10 @@ def b64_to_dict(payload: str) -> dict["str", Any]: payload += padding json_bytes = base64.urlsafe_b64decode(payload) return json.loads(json_bytes.decode("utf-8")) + +def is_member(chatmember: ResultChatMemberUnion): + return any([ + isinstance(chatmember, ChatMemberOwner), + isinstance(chatmember, ChatMemberAdministrator), + isinstance(chatmember, ChatMemberMember), + ]) diff --git a/repositories/products.py b/repositories/products.py index 8d9dcaf..bab3880 100644 --- a/repositories/products.py +++ b/repositories/products.py @@ -90,9 +90,7 @@ class ProductRepository: *, include_hidden: bool = False, ) -> int: - stmt = select(func.count()).select_from(Product).where( - Product.category_id == category_id - ) + stmt = select(func.count()).select_from(Product).where(Product.category_id == category_id) if not include_hidden: stmt = stmt.where(Product.is_hidden.is_(False)) @@ -108,8 +106,10 @@ class ProductRepository: ) -> int: ts_query = func.plainto_tsquery("simple", query) - stmt = select(func.count()).select_from(Product).where( - Product.search_vector.op("@@")(ts_query) + stmt = ( + select(func.count()) + .select_from(Product) + .where(Product.search_vector.op("@@")(ts_query)) ) if not include_hidden: