separated clients handlers, cart->checkout ppline

This commit is contained in:
2026-02-11 20:53:47 +07:00
parent 6409d66dfd
commit a3aaafd7ac
20 changed files with 658 additions and 369 deletions

View File

@@ -0,0 +1,15 @@
from .menu import router as menu_router
from .catalogue import router as catalogue_router
from .products import router as products_router
from .cart import router as cart_router
from .checkout import router as checkout_router
from .security import router as security_router
routers = [
menu_router,
catalogue_router,
products_router,
cart_router,
checkout_router,
security_router,
]

80
handlers/client/cart.py Normal file
View File

@@ -0,0 +1,80 @@
from aiogram import Router, F
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from config import PAGE_SIZE
from misc.kb.client import (
render_cart,
)
from services.orders import OrderService
router = Router()
@router.callback_query(F.data == "cart")
async def cart_init(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
order_service: OrderService,
):
await state.clear()
cart = await order_service.build_pagination_cart_dto(
session, customer=cb.from_user.id
)
await cb.message.edit_text(
f"total: {cart.total}",
reply_markup=render_cart(
cart.items[:PAGE_SIZE], show_next=len(cart.items) > PAGE_SIZE
),
)
@router.message(Command("cart"))
async def cart_cmd(
msg: Message,
state: FSMContext,
session: AsyncSession,
order_service: OrderService,
):
await state.clear()
cart = await order_service.build_pagination_cart_dto(
session, customer=msg.from_user.id
)
await msg.answer(
f"total: {cart.total}",
reply_markup=render_cart(
cart.items[:PAGE_SIZE], show_next=len(cart.items) > PAGE_SIZE
),
)
@router.callback_query(F.data.startswith("cart:"))
async def cart_pagination(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
order_service: OrderService,
):
await state.clear()
page = int(cb.data.split(":")[1])
cart = await order_service.build_pagination_cart_dto(
session, customer=cb.from_user.id, offset=page * PAGE_SIZE
)
await cb.message.edit_reply_markup(
reply_markup=render_cart(
cart.items[:PAGE_SIZE],
page=page,
show_next=len(cart.items) > PAGE_SIZE,
show_prev=page > 0,
),
)

View File

@@ -0,0 +1,91 @@
from aiogram import Router, F
from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from config import PAGE_SIZE
from misc.kb import render_category
from misc.kb.client import (
get_back_to_catalogue,
render_products,
)
from repositories.categories import CategoriesRepository
from repositories.products import ProductRepository
router = Router()
@router.callback_query(F.data.startswith("cat:"))
async def subcatalogue(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
categories_repo: CategoriesRepository,
products_repo: ProductRepository,
):
await state.clear()
cat_id = cb.data.split(":")[1]
if cat_id == "root":
children = await categories_repo.get_categories_by_parent_id(session)
kb = render_category(children, show_menu=True)
await cb.message.edit_text(
"category selection",
reply_markup=kb,
)
return
cat_id = int(cat_id)
children = await categories_repo.get_categories_by_parent_id(session, cat_id)
category = await categories_repo.get_category_by_id(session, cat_id)
parent_id = category.parent_id or "root"
if not children:
products = await products_repo.get_product_by_category(
session, cat_id, limit=PAGE_SIZE + 1, offset=0
)
products_len = len(products)
if not products:
await cb.message.edit_text(
"🍃 Здесь ничего нет...",
reply_markup=get_back_to_catalogue(parent_id),
)
return
if cb.message.photo:
await cb.message.delete()
await cb.message.answer(
"hya",
reply_markup=render_products(
products[:PAGE_SIZE],
parent_id=parent_id,
cat_id=cat_id,
page=0,
show_prev=products_len < 0,
show_next=products_len > PAGE_SIZE,
),
)
return
await cb.message.edit_text(
"hya",
reply_markup=render_products(
products[:PAGE_SIZE],
parent_id=parent_id,
cat_id=cat_id,
page=0,
show_prev=products_len < 0,
show_next=products_len > PAGE_SIZE,
),
)
return
kb = render_category(children, parent_id)
await cb.message.edit_text(
"category selection",
reply_markup=kb,
)

122
handlers/client/checkout.py Normal file
View File

@@ -0,0 +1,122 @@
from aiogram import Router, F
from aiogram.types import CallbackQuery, Message
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from dto.checkout import CheckoutContext
from misc.kb.client import (
order_confirmation,
order_specs_confirmation,
return_menu,
)
from misc.states import CheckoutStorage
from misc.texts import get_order_item_list
from misc.utils import is_valid_phone
from repositories.orders import OrderRepository
from services.orders import OrderService
router = Router()
@router.callback_query(F.data == "checkout")
async def checkout(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
order_service: OrderService,
):
await state.clear()
cart = await order_service.build_full_cart_dto(session, cb.from_user.id)
await cb.message.edit_text(
text=get_order_item_list(cart), reply_markup=order_confirmation(cart.order_id)
)
@router.callback_query(F.data.startswith("confirm_order:"))
async def order_confirm(
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
order = await order_repo.get_order_by_id(session, int(order_id))
if not order or order.customer != cb.from_user.id:
await cb.message.edit_text("🍃 Что-то пошло не так.", reply_markup=return_menu)
return
await cb.message.edit_text("<b>👤 Укажите ваше имя:</b>", reply_markup=return_menu)
ctx = CheckoutContext(orig_msg=cb.message, order_id=order.id)
await state.set_state(CheckoutStorage.name)
await state.set_data({"ctx": ctx})
@router.message(CheckoutStorage.name)
async def checkout_name(msg: Message, state: FSMContext):
data = await state.get_data()
ctx: CheckoutContext = data.get("ctx", CheckoutContext)
await state.clear()
ctx.name = msg.text
await msg.delete()
await ctx.orig_msg.edit_text(
f"<i>👤 Имя: {ctx.name}</i>\n\n<b>📱 Укажите ваш номер телефона:</b>",
reply_markup=return_menu,
)
await state.set_state(CheckoutStorage.phone)
await state.set_data({"ctx": ctx})
@router.message(CheckoutStorage.phone)
async def checkout_phone(msg: Message, state: FSMContext):
data = await state.get_data()
ctx: CheckoutContext = data.get("ctx", CheckoutContext)
await state.clear()
await msg.delete()
if not is_valid_phone(msg.text):
await ctx.orig_msg.edit_text(
f"❌ <i>{msg.text}</i> не является действительным номером, попробуйте ещё раз.",
reply_markup=return_menu,
)
await state.set_state(CheckoutStorage.phone)
await state.set_data({"ctx": ctx})
return
ctx.phone = msg.text
await ctx.orig_msg.edit_text(
f"<i>👤 Имя: {ctx.name}</i>\n<i>📱 Номер телефона: {ctx.phone}</i>\n\n<b>📍 Укажите ваш адрес (город, улица):</b>",
reply_markup=return_menu,
)
await state.set_state(CheckoutStorage.address)
await state.set_data({"ctx": ctx})
@router.message(CheckoutStorage.address)
async def checkout_address(msg: Message, state: FSMContext):
data = await state.get_data()
ctx: CheckoutContext = data.get("ctx", CheckoutContext)
await state.clear()
await msg.delete()
ctx.address = msg.text
await ctx.orig_msg.edit_text(
"<b>‼️ Проверьте правильность ваших данных.</b>\n\n"
"━━━━━━━━━━━━━━\n\n"
f"<i>👤 Имя: {ctx.name}</i>\n"
f"<i>📱 Номер телефона: {ctx.phone}</i>\n"
f"<i>📍 Ваш адрес: {ctx.address}</i>\n\n"
"━━━━━━━━━━━━━━",
reply_markup=order_specs_confirmation(ctx.order_id),
) # TODO: State-driven Payment System.

37
handlers/client/menu.py Normal file
View File

@@ -0,0 +1,37 @@
from aiogram import Router, F
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from misc.kb import main_menu_kb
from services.orders import OrderService
router = Router()
@router.message(Command("start"))
async def main_menu(
msg: Message, state: FSMContext, session: AsyncSession, order_service: OrderService
):
await state.clear()
cart_items = await order_service.get_cart_items_count(session, msg.from_user.id)
await msg.answer(
"hii!", reply_markup=main_menu_kb(cart_items)
) # TODO: Write a welcome message
@router.callback_query(F.data.startswith("menu:"))
async def main_menu_cb(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
order_service: OrderService,
):
await state.clear()
cart_items = await order_service.get_cart_items_count(session, cb.from_user.id)
await cb.message.edit_text(
"hii!", reply_markup=main_menu_kb(cart_items)
) # TODO: Write a welcome message

164
handlers/client/products.py Normal file
View File

@@ -0,0 +1,164 @@
import os
from re import Match
from aiogram import Router, F
from aiogram.types import CallbackQuery, FSInputFile
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from config import PAGE_SIZE
from misc.kb.client import (
render_product_interactions,
render_products,
)
from misc.texts import get_product_description
from repositories.categories import CategoriesRepository
from repositories.products import ProductRepository
from services.orders import OrderService
router = Router()
@router.callback_query(
F.data.regexp(r"products:(?P<category_id>.*):(?P<page_id>\d+)").as_(
"pagination_match"
)
)
async def products_pagination(
cb: CallbackQuery,
state: FSMContext,
pagination_match: Match[str],
session: AsyncSession,
products_repo: ProductRepository,
categories_repo: CategoriesRepository,
):
await state.clear()
pagination_data = pagination_match.groupdict()
category_id = int(pagination_data.get("category_id"))
category = await categories_repo.get_category_by_id(session, category_id)
page_id = int(pagination_data.get("page_id", 0))
products = await products_repo.get_product_by_category(
session, category_id, limit=PAGE_SIZE + 1, offset=page_id * PAGE_SIZE
)
products_len = len(products)
await cb.message.edit_reply_markup(
reply_markup=render_products(
products[:PAGE_SIZE],
parent_id=category.parent_id,
cat_id=category_id,
page=page_id,
show_prev=page_id > 0,
show_next=products_len > PAGE_SIZE,
),
)
@router.callback_query(F.data.startswith("product:"))
async def product_card(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
products_repo: ProductRepository,
order_service: OrderService,
):
await state.clear()
product_id = int(cb.data.split(":")[1])
product = await products_repo.get_product_by_id(session, product_id)
order_item = await order_service.get_cart_product_amount(
session, customer=cb.from_user.id, product_id=product_id
)
if product.file_id:
await cb.message.delete()
msg = await cb.message.answer_photo(
product.file_id,
caption=get_product_description(product),
reply_markup=render_product_interactions(
product_id=product.id,
category_id=product.category_id,
show_cart=bool(order_item),
cart_amount=order_item,
),
)
return
if not os.path.isfile(product.img_path):
await cb.message.edit_text(
get_product_description(product),
reply_markup=render_product_interactions(
product_id=product.id,
category_id=product.category_id,
show_cart=bool(order_item),
cart_amount=order_item,
),
)
return
await cb.message.delete()
msg = await cb.message.answer_photo(
FSInputFile(product.img_path),
caption=get_product_description(product),
reply_markup=render_product_interactions(
product_id=product.id,
category_id=product.category_id,
show_cart=bool(order_item),
cart_amount=order_item,
),
)
if not msg.photo:
return # TODO: Add logging
await products_repo.add_product_file_id(
session, product_id, file_id=msg.photo[-1].file_id
)
@router.callback_query(F.data.startswith("cart_action:"))
async def add_to_cart(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
order_service: OrderService,
products_repo: ProductRepository,
):
await state.clear()
action = cb.data.split(":")[1]
product_id = int(cb.data.split(":")[2])
product = await products_repo.get_product_by_id(session, product_id)
show_cart = True
if action == "add":
quantity = await order_service.add_to_cart(
session, customer=cb.from_user.id, product_id=product_id
)
elif action == "remove":
quantity = await order_service.remove_from_cart(
session, customer=cb.from_user.id, product_id=product_id
)
show_cart = bool(quantity)
else:
show_cart = False
quantity = 0
await order_service.clear_cart_item(
session, customer=cb.from_user.id, product_id=product_id
)
try:
await cb.message.edit_reply_markup(
reply_markup=render_product_interactions(
product_id=product_id,
category_id=product.category_id,
show_cart=show_cart,
cart_amount=quantity,
)
)
except Exception as e:
await cb.answer("🍃")
raise e

View File

@@ -0,0 +1,20 @@
from aiogram import Router, F
from aiogram.enums import MessageOriginType
from aiogram.types import Message
from config import VERIFIED_ACCOUNTS
router = Router()
@router.message(F.forward_origin)
async def security_check(msg: Message):
if (
msg.forward_origin.type == MessageOriginType.USER
and msg.forward_origin.sender_user.id in VERIFIED_ACCOUNTS
):
text = "🍃 Вы общаетесь с оффициальным аккаунтом Екатерины."
else:
text = "<b>❌ Вы общаетесь с подставным лицом. Мы советуем закончить диалог с мошенником как можно быстрее.</b>"
await msg.answer(text)