feat: channel sub
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -177,4 +177,5 @@ cython_debug/
|
|||||||
|
|
||||||
# Static / IMG
|
# Static / IMG
|
||||||
static/img
|
static/img
|
||||||
static/*
|
static/*
|
||||||
|
.env.prod
|
||||||
@@ -29,6 +29,11 @@ REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/1")
|
|||||||
PROXY_URL = os.getenv("PROXY_URL")
|
PROXY_URL = os.getenv("PROXY_URL")
|
||||||
|
|
||||||
WELCOME_VIDEO_NOTE_FP = os.getenv("WELCOME_VIDEO_NOTE_FP", "static/welcome.mp4")
|
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 ###
|
### Constants ###
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ from aiogram.fsm.context import FSMContext
|
|||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import CallbackQuery, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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 db.models.orders import OrderStatus
|
||||||
from dto.checkout import CheckoutContext
|
from dto.checkout import CheckoutContext
|
||||||
from misc.kb.admins import customer_contacts
|
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.kb.common import return_menu
|
||||||
from misc.states import CheckoutStorage
|
from misc.states import CheckoutStorage
|
||||||
from misc.texts import get_order_item_list
|
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 repositories.orders import OrderRepository
|
||||||
from services.orders import OrderService
|
from services.orders import OrderService
|
||||||
|
|
||||||
@@ -180,3 +180,30 @@ async def payment_create(
|
|||||||
)
|
)
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return
|
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(
|
||||||
|
"<b>📚 Мы заметили, что вы не подписаны на канал.</b>",
|
||||||
|
reply_markup=subscription(order_id),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await order_confirm(
|
||||||
|
cb=cb,
|
||||||
|
state=state,
|
||||||
|
session=session,
|
||||||
|
order_repo=order_repo,
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from collections.abc import Callable
|
|||||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
import config
|
||||||
from config import PAGE_SIZE
|
from config import PAGE_SIZE
|
||||||
from db.models import Category, Product
|
from db.models import Category, Product
|
||||||
from dto.cart import CartItemDTO
|
from dto.cart import CartItemDTO
|
||||||
@@ -81,6 +82,7 @@ def main_menu_kb(cart_items: int) -> InlineKeyboardMarkup:
|
|||||||
InlineKeyboardButton(text="🔍 Поиск", callback_data="find"),
|
InlineKeyboardButton(text="🔍 Поиск", callback_data="find"),
|
||||||
],
|
],
|
||||||
[cart_btn(cart_items)],
|
[cart_btn(cart_items)],
|
||||||
|
[InlineKeyboardButton(text="🌸 Подписаться на канал", url=config.CHANNEL_URL)],
|
||||||
]
|
]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
@@ -168,9 +170,7 @@ def render_products(
|
|||||||
nav_buttons = []
|
nav_buttons = []
|
||||||
if prev_cb:
|
if prev_cb:
|
||||||
nav_buttons.append(InlineKeyboardButton(text="◀️", callback_data=prev_cb))
|
nav_buttons.append(InlineKeyboardButton(text="◀️", callback_data=prev_cb))
|
||||||
page_text = (
|
page_text = f"{page + 1}/{total_pages}" if total_pages is not None else f"{page + 1}"
|
||||||
f"{page + 1}/{total_pages}" if total_pages is not None else f"{page + 1}"
|
|
||||||
)
|
|
||||||
nav_buttons.append(InlineKeyboardButton(text=page_text, callback_data="..."))
|
nav_buttons.append(InlineKeyboardButton(text=page_text, callback_data="..."))
|
||||||
if next_cb:
|
if next_cb:
|
||||||
nav_buttons.append(InlineKeyboardButton(text="▶️", callback_data=next_cb))
|
nav_buttons.append(InlineKeyboardButton(text="▶️", callback_data=next_cb))
|
||||||
@@ -298,7 +298,7 @@ def order_confirmation(order_id: int) -> InlineKeyboardMarkup:
|
|||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text="🟢 Подтвердить заказ",
|
text="🟢 Подтвердить заказ",
|
||||||
callback_data=f"confirm_order:{order_id}",
|
callback_data=f"precheck_order:{order_id}",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
[InlineKeyboardButton(text="❌ Отменить", callback_data="menu:main")],
|
[InlineKeyboardButton(text="❌ Отменить", callback_data="menu:main")],
|
||||||
@@ -306,6 +306,22 @@ def order_confirmation(order_id: int) -> InlineKeyboardMarkup:
|
|||||||
).as_markup()
|
).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:
|
def order_specs_confirmation(order_id: int) -> InlineKeyboardMarkup:
|
||||||
return InlineKeyboardBuilder(
|
return InlineKeyboardBuilder(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import json
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import phonenumbers
|
import phonenumbers
|
||||||
|
from aiogram.types import (
|
||||||
|
ChatMemberAdministrator,
|
||||||
|
ChatMemberMember,
|
||||||
|
ChatMemberOwner,
|
||||||
|
ResultChatMemberUnion,
|
||||||
|
)
|
||||||
from phonenumbers import NumberParseException
|
from phonenumbers import NumberParseException
|
||||||
|
|
||||||
|
|
||||||
@@ -26,3 +32,10 @@ def b64_to_dict(payload: str) -> dict["str", Any]:
|
|||||||
payload += padding
|
payload += padding
|
||||||
json_bytes = base64.urlsafe_b64decode(payload)
|
json_bytes = base64.urlsafe_b64decode(payload)
|
||||||
return json.loads(json_bytes.decode("utf-8"))
|
return json.loads(json_bytes.decode("utf-8"))
|
||||||
|
|
||||||
|
def is_member(chatmember: ResultChatMemberUnion):
|
||||||
|
return any([
|
||||||
|
isinstance(chatmember, ChatMemberOwner),
|
||||||
|
isinstance(chatmember, ChatMemberAdministrator),
|
||||||
|
isinstance(chatmember, ChatMemberMember),
|
||||||
|
])
|
||||||
|
|||||||
@@ -90,9 +90,7 @@ class ProductRepository:
|
|||||||
*,
|
*,
|
||||||
include_hidden: bool = False,
|
include_hidden: bool = False,
|
||||||
) -> int:
|
) -> int:
|
||||||
stmt = select(func.count()).select_from(Product).where(
|
stmt = select(func.count()).select_from(Product).where(Product.category_id == category_id)
|
||||||
Product.category_id == category_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if not include_hidden:
|
if not include_hidden:
|
||||||
stmt = stmt.where(Product.is_hidden.is_(False))
|
stmt = stmt.where(Product.is_hidden.is_(False))
|
||||||
@@ -108,8 +106,10 @@ class ProductRepository:
|
|||||||
) -> int:
|
) -> int:
|
||||||
ts_query = func.plainto_tsquery("simple", query)
|
ts_query = func.plainto_tsquery("simple", query)
|
||||||
|
|
||||||
stmt = select(func.count()).select_from(Product).where(
|
stmt = (
|
||||||
Product.search_vector.op("@@")(ts_query)
|
select(func.count())
|
||||||
|
.select_from(Product)
|
||||||
|
.where(Product.search_vector.op("@@")(ts_query))
|
||||||
)
|
)
|
||||||
|
|
||||||
if not include_hidden:
|
if not include_hidden:
|
||||||
|
|||||||
Reference in New Issue
Block a user