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

@@ -17,3 +17,4 @@ POSTGRES_URL = os.getenv(
### Constants ###
PAGE_SIZE = 4
VERIFIED_ACCOUNTS = [1026030711]

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
@@ -17,3 +18,4 @@ class CartItemDTO:
class CartDTO:
items: list[CartItemDTO]
total: int
order_id: Optional[int] = None

18
dto/checkout.py Normal file
View File

@@ -0,0 +1,18 @@
from dataclasses import dataclass
from typing import Optional
from aiogram.types import Message
@dataclass
class CheckoutContext:
orig_msg: Message
order_id: int
name: Optional[str] = None
phone: Optional[str] = None
address: Optional[str] = None
@dataclass
class PaymentContext:
notification_msg: Message

View File

@@ -1,9 +1,4 @@
from aiogram import Dispatcher
from .client import routers as client_routers
from .admins import routers as admins_routers
from .client import router as client_router
from .admins import router as admins_router
def init_handlers(dp: Dispatcher):
dp.include_router(client_router)
dp.include_router(admins_router)
__all__ = ["client_routers", "admins_routers"]

View File

@@ -1,9 +0,0 @@
from aiogram import Router
from aiogram.types import Message
router = Router()
@router.message()
async def echo(msg: Message):
await msg.answer(msg.text)

View File

@@ -0,0 +1 @@
routers = []

View File

@@ -1,347 +0,0 @@
import os
from re import Match
from aiogram import Router, F
from aiogram.types import CallbackQuery, FSInputFile, 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 import main_menu_kb, render_category
from misc.kb.client import (
get_back_to_catalogue,
render_cart,
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.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
@router.message(Command("orders"))
async def get_orders(
msg: Message,
state: FSMContext,
order_service: OrderService,
session: AsyncSession,
):
await state.clear()
count = await order_service.get_order_count(session, msg.from_user.id)
await msg.answer(f"you have {count} orders.")
@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,
)
@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
@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_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_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_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,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)

View File

@@ -5,7 +5,7 @@ from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from config import BOT_TOKEN
from handlers import init_handlers
from handlers import admins_routers, client_routers
from middlewares.di import DIMiddleware
from middlewares.repository import RepositoryMiddleware
@@ -61,7 +61,11 @@ async def main():
)
)
init_handlers(dp)
for ar in admins_routers:
dp.include_router(ar)
for cr in client_routers:
dp.include_router(cr)
await dp.start_polling(bot)

View File

@@ -6,6 +6,10 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from db.models import Category, Product
from dto.cart import CartItemDTO
return_menu = InlineKeyboardBuilder(
[[InlineKeyboardButton(text="⬅️ В меню", callback_data="menu:main")]]
).as_markup()
def pagination_row(
*,
@@ -15,6 +19,9 @@ def pagination_row(
) -> list[InlineKeyboardButton]:
row = []
if not (prev_cb or next_cb):
return row
if prev_cb:
row.append(InlineKeyboardButton(text="⬅️", callback_data=prev_cb))
@@ -161,7 +168,7 @@ def render_cart(
) -> InlineKeyboardMarkup:
btns = [
InlineKeyboardButton(
text=f"{cart_item.quantity} | {cart_item.name} | {cart_item.total}",
text=f"🛒 {cart_item.quantity} | {cart_item.name} | {cart_item.total}",
callback_data=f"product:{cart_item.product_id}",
)
for cart_item in cart_items
@@ -184,4 +191,33 @@ def render_cart(
]
+ [btns[i : i + 2] for i in range(0, len(cart_items), 2)]
+ [nav]
+ [[InlineKeyboardButton(text="🔷 Оформить заказ", callback_data="checkout")]]
).as_markup()
def order_confirmation(order_id: int) -> InlineKeyboardMarkup:
return InlineKeyboardBuilder(
[
[
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(
[
[
InlineKeyboardButton(
text="🟢 Перейти к оплате",
callback_data=f"payment:{order_id}",
)
],
[InlineKeyboardButton(text="", callback_data="menu:main")],
]
).as_markup()

7
misc/states.py Normal file
View File

@@ -0,0 +1,7 @@
from aiogram.fsm.state import StatesGroup, State
class CheckoutStorage(StatesGroup):
name = State()
phone = State()
address = State()

View File

@@ -1,4 +1,5 @@
from db.models import Product
from dto.cart import CartDTO
def get_product_description(product: Product) -> str:
@@ -7,3 +8,18 @@ def get_product_description(product: Product) -> str:
output += f"\n\n📜 {product.description}"
return output
def get_order_item_list(cart: CartDTO):
text = "🛍 Состав заказа:\n"
for i, product in enumerate(cart.items):
text += f"\n{i+1}. <b>{product.name}</b>\n\t<i>{product.quantity} × {product.price} = {product.total}₽</i>\n"
text += f"\n━━━━━━━━━━━━━━\n💰 Итого: {cart.total}"
return text
# def get_order_notification(cart: CartDTO):
# text
# FIXME

10
misc/utils.py Normal file
View File

@@ -0,0 +1,10 @@
import phonenumbers
from phonenumbers import NumberParseException
def is_valid_phone(phone: str, region="RU") -> bool:
try:
parsed = phonenumbers.parse(phone, region)
return phonenumbers.is_valid_number(parsed)
except NumberParseException:
return False

View File

@@ -156,7 +156,7 @@ class OrderService:
return await self.order_items_repo.get_items_count(session, order_id=order.id)
async def build_cart_dto(
async def build_pagination_cart_dto(
self,
session: AsyncSession,
*,
@@ -190,4 +190,30 @@ class OrderService:
cart_items.append(cart_item)
total += cart_item.total
return CartDTO(items=cart_items, total=total)
return CartDTO(items=cart_items, total=total, order_id=order.id)
async def build_full_cart_dto(self, session: AsyncSession, customer: int):
order = await self.order_repo.get_draft_order_by_user(session, customer)
cart_items = []
total = 0
if not order:
return CartDTO(items=cart_items, total=total)
for cart_item in order.items:
if cart_item.quantity <= 0:
await self.order_items_repo.delete_item(session, item_id=cart_item.id)
continue
dto = CartItemDTO(
product_id=cart_item.product.id,
name=cart_item.product.name,
quantity=cart_item.quantity,
price=cart_item.product.price,
)
cart_items.append(dto)
total += dto.total
return CartDTO(cart_items, total=total, order_id=order.id)