product management, unification of kb and so on.

This commit is contained in:
2026-02-24 20:47:52 +07:00
parent 36ffca460f
commit 035170e46f
23 changed files with 470 additions and 130 deletions

10
dto/control.py Normal file
View File

@@ -0,0 +1,10 @@
from dataclasses import dataclass
from aiogram.types import Message
@dataclass
class EditProductContext:
product_id: int
mode: str
msg: Message

View File

@@ -1,3 +1,5 @@
from .inline_mode import router as inline_router from .inline_mode import router as inline_router
from .menu import router as menu_router
from .product_mgmt import router as product_router
routers = [inline_router] routers = [inline_router, menu_router, product_router]

View File

@@ -11,7 +11,8 @@ from aiogram.utils.deep_linking import create_start_link
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from misc.filters import IsVerified from misc.filters import IsVerified
from misc.kb.admins import payment_link, placeholder_kb from misc.kb.admins import payment_link
from misc.kb.common import placeholder_kb
from misc.utils import b64_to_dict, dict_to_b64 from misc.utils import b64_to_dict, dict_to_b64
from repositories.invoices import InvoiceRepository from repositories.invoices import InvoiceRepository

31
handlers/admins/menu.py Normal file
View File

@@ -0,0 +1,31 @@
import logging
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery
from aiogram.filters import Command
from misc.filters import IsVerified
from misc.kb.admins import main_menu
router = Router()
logger = logging.getLogger(__name__)
@router.message(IsVerified(), Command("start"))
async def admin_main_menu(
msg: Message,
state: FSMContext,
):
await state.clear()
await msg.answer("admin", reply_markup=main_menu)
@router.callback_query(IsVerified(), F.data.startswith("menu:"))
async def admin_main_menu_cb(
cb: CallbackQuery,
state: FSMContext,
):
await state.clear()
await cb.message.edit_text("admin", reply_markup=main_menu)

View File

@@ -0,0 +1,95 @@
import logging
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from dto.control import EditProductContext
from misc.filters import IsVerified
from misc.kb.admins import back_to_product_kb
from misc.states import AdminControlStorage
from misc.texts import product_editing_mapping
from repositories.products import ProductRepository
logger = logging.getLogger(__name__)
router = Router()
@router.callback_query(IsVerified(), F.data.startswith("edit_product:"))
async def edit_product(cb: CallbackQuery, state: FSMContext):
await state.clear()
data = cb.data.split(":")
product_id = data[1]
mode = data[2]
if cb.message.photo:
await cb.message.delete()
msg = await cb.message.answer(
f"<b>✍️ Введите {product_editing_mapping.get(mode)}</b>",
reply_markup=back_to_product_kb(
product_id, cb_factory=lambda p: f"product:{p}"
),
)
else:
msg = cb.message
await cb.message.edit_text(
f"<b>✍️ Введите {product_editing_mapping.get(mode)}</b>",
reply_markup=back_to_product_kb(
product_id, cb_factory=lambda p: f"product:{p}"
),
)
ctx = EditProductContext(int(product_id), mode, msg)
await state.set_state(AdminControlStorage.edit_product)
await state.set_data({"ctx": ctx})
@router.message(AdminControlStorage.edit_product)
async def edit_product_query(
msg: Message,
state: FSMContext,
session: AsyncSession,
products_repo: ProductRepository,
):
data = await state.get_data()
await state.clear()
await msg.delete()
ctx: EditProductContext = data.get("ctx", EditProductContext)
kb = back_to_product_kb(ctx.product_id, cb_factory=lambda p: f"product:{p}")
try:
await ctx.msg.edit_text("")
if ctx.mode == "name":
await products_repo.update_product_name_by_id(
session, ctx.product_id, msg.text
)
elif ctx.mode == "description":
await products_repo.update_product_description_by_id(
session, ctx.product_id, msg.text
)
elif ctx.mode == "price":
if msg.text.isdigit():
await products_repo.update_product_price_by_id(
session, ctx.product_id, int(msg.text)
)
else:
await ctx.msg.edit_text(
"❌ Вы ввели не число, попробуйте ещё раз...",
reply_markup=kb,
)
await state.set_state(AdminControlStorage.edit_product)
await state.set_data({"ctx": ctx})
return
else:
await ctx.msg.edit_text("🍃", reply_markup=kb)
return
await ctx.msg.edit_text("✅ Изменения применены.", reply_markup=kb)
except Exception as e:
logger.exception(e)
await ctx.msg.edit_text(text="‼️ Что-то пошло не так.", reply_markup=kb)

View File

@@ -5,6 +5,7 @@ from .products import router as products_router
from .cart import router as cart_router from .cart import router as cart_router
from .checkout import router as checkout_router from .checkout import router as checkout_router
from .security import router as security_router from .security import router as security_router
from .search import router as search_router
routers = [ routers = [
invoice_router, invoice_router,
@@ -14,4 +15,5 @@ routers = [
cart_router, cart_router,
checkout_router, checkout_router,
security_router, security_router,
search_router,
] ]

View File

@@ -5,9 +5,8 @@ from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import PAGE_SIZE from config import PAGE_SIZE
from misc.kb.client import ( from misc.kb.client import render_cart
render_cart, from misc.kb.common import return_menu
)
from services.orders import OrderService from services.orders import OrderService
router = Router() router = Router()
@@ -25,11 +24,16 @@ async def cart_init(
cart = await order_service.build_pagination_cart_dto( cart = await order_service.build_pagination_cart_dto(
session, customer=cb.from_user.id session, customer=cb.from_user.id
) )
if not len(cart.items):
await cb.message.edit_text("🛒 Корзина пуста.", reply_markup=return_menu)
return
await cb.message.edit_text( await cb.message.edit_text(
f"total: {cart.total}", f"total: {cart.total} | {len(cart.items)}",
reply_markup=render_cart( reply_markup=render_cart(
cart.items[:PAGE_SIZE], show_next=len(cart.items) > PAGE_SIZE cart.items[:PAGE_SIZE],
show_next=len(cart.items) > PAGE_SIZE,
show_purchase=bool(len(cart.items)),
), ),
) )
@@ -47,10 +51,16 @@ async def cart_cmd(
session, customer=msg.from_user.id session, customer=msg.from_user.id
) )
if not len(cart.items):
await msg.answer("🛒 Корзина пуста.", reply_markup=return_menu)
return
await msg.answer( await msg.answer(
f"total: {cart.total}", f"total: {cart.total}",
reply_markup=render_cart( reply_markup=render_cart(
cart.items[:PAGE_SIZE], show_next=len(cart.items) > PAGE_SIZE cart.items[:PAGE_SIZE],
show_next=len(cart.items) > PAGE_SIZE,
show_purchase=bool(len(cart.items)),
), ),
) )

View File

@@ -29,7 +29,9 @@ async def subcatalogue(
if cat_id == "root": if cat_id == "root":
children = await categories_repo.get_categories_by_parent_id(session) children = await categories_repo.get_categories_by_parent_id(session)
kb = render_category(children, show_menu=True) kb = render_category(
children, show_menu=True, category_cb_factory=lambda c: f"cat:{c.id}"
)
await cb.message.edit_text( await cb.message.edit_text(
"category selection", "category selection",
reply_markup=kb, reply_markup=kb,
@@ -61,11 +63,14 @@ async def subcatalogue(
"hya", "hya",
reply_markup=render_products( reply_markup=render_products(
products[:PAGE_SIZE], products[:PAGE_SIZE],
parent_id=parent_id, product_cb_factory=lambda p: f"product:{p.id}",
cat_id=cat_id, back_cb=f"cat:{parent_id}",
page=0, next_cb=(
show_prev=products_len < 0, f"products:{cat_id}:{0 + 1}"
show_next=products_len > PAGE_SIZE, if products_len > PAGE_SIZE
else None
),
columns=2,
), ),
) )
return return
@@ -74,16 +79,20 @@ async def subcatalogue(
"hya", "hya",
reply_markup=render_products( reply_markup=render_products(
products[:PAGE_SIZE], products[:PAGE_SIZE],
parent_id=parent_id, product_cb_factory=lambda p: f"product:{p.id}",
cat_id=cat_id, back_cb=f"cat:{parent_id}",
page=0, prev_cb=f"products:{cat_id}:{0 - 1}" if products_len < 0 else None,
show_prev=products_len < 0, next_cb=(
show_next=products_len > PAGE_SIZE, f"products:{cat_id}:{0 + 1}" if products_len > PAGE_SIZE else None
),
columns=2,
), ),
) )
return return
kb = render_category(children, parent_id) kb = render_category(
children, parent_id, category_cb_factory=lambda c: f"cat:{c.id}"
)
await cb.message.edit_text( await cb.message.edit_text(
"category selection", "category selection",

View File

@@ -6,13 +6,11 @@ from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import NOTIFICATION_CHANNEL from config import NOTIFICATION_CHANNEL
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 ( from misc.kb.client import order_confirmation, order_specs_confirmation
order_confirmation, from misc.kb.common import return_menu
order_specs_confirmation,
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_valid_phone
@@ -152,13 +150,6 @@ async def payment_create(
await state.clear() await state.clear()
try: try:
await cb.message.edit_text(
"<b>🟢 Ваш заказ успешно отправлен!</b>\n"
"<i>📦 Мы свяжемся с вами для уточнения деталей в ближайшее время.</i>\n\n"
"<b>‼️ Чтобы убедиться, что вы общаетесь с доверенным контактом ШВЕЙТЕХЦЕНТР, перешлите любое сообщение человека в этот чат.</b>",
reply_markup=return_menu,
)
cart = await order_service.build_full_cart_dto(session, cb.from_user.id) cart = await order_service.build_full_cart_dto(session, cb.from_user.id)
await cb.bot.send_message( await cb.bot.send_message(
NOTIFICATION_CHANNEL, NOTIFICATION_CHANNEL,
@@ -173,7 +164,17 @@ async def payment_create(
cb.from_user.id, bool(cb.from_user.username), cb.from_user.username cb.from_user.id, bool(cb.from_user.username), cb.from_user.username
), ),
) )
await order_repo.remove_order_by_customer(session, cb.from_user.id) await order_repo.update_order_status(
session, cart.order_id, OrderStatus.CREATED
)
await cb.message.edit_text(
"<b>🟢 Ваш заказ успешно отправлен!</b>\n"
"<i>📦 Мы свяжемся с вами для уточнения деталей в ближайшее время.</i>\n\n"
"<b>‼️ Чтобы убедиться, что вы общаетесь с доверенным контактом ШВЕЙТЕХЦЕНТР, перешлите любое сообщение человека в этот чат.</b>",
reply_markup=return_menu,
)
except Exception as e: except Exception as e:
await cb.message.edit_text( await cb.message.edit_text(
"🍃 Что-то пошло не так, повторите попытку позже...", "🍃 Что-то пошло не так, повторите попытку позже...",

View File

@@ -11,7 +11,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from db.models.invoices import InvoiceStatus from db.models.invoices import InvoiceStatus
from misc.kb.admins import verify_payment from misc.kb.admins import verify_payment
from misc.kb.client import confirm_payment, return_menu from misc.kb.client import confirm_payment
from misc.kb.common import return_menu
from repositories.invoices import InvoiceRepository from repositories.invoices import InvoiceRepository
from config import card_info from config import card_info

View File

@@ -12,7 +12,7 @@ router = Router()
@router.message(Command("start")) @router.message(Command("start"))
async def main_menu( async def user_main_menu(
msg: Message, msg: Message,
state: FSMContext, state: FSMContext,
session: AsyncSession, session: AsyncSession,
@@ -29,7 +29,7 @@ async def main_menu(
@router.callback_query(F.data.startswith("menu:")) @router.callback_query(F.data.startswith("menu:"))
async def main_menu_cb( async def user_main_menu_cb(
cb: CallbackQuery, cb: CallbackQuery,
state: FSMContext, state: FSMContext,
session: AsyncSession, session: AsyncSession,

View File

@@ -5,7 +5,8 @@ from aiogram.types import CallbackQuery, FSInputFile
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import PAGE_SIZE from config import PAGE_SIZE, VERIFIED_ACCOUNTS
from misc.kb import admins
from misc.kb.client import ( from misc.kb.client import (
render_product_interactions, render_product_interactions,
render_products, render_products,
@@ -46,11 +47,15 @@ async def products_pagination(
await cb.message.edit_reply_markup( await cb.message.edit_reply_markup(
reply_markup=render_products( reply_markup=render_products(
products[:PAGE_SIZE], products[:PAGE_SIZE],
parent_id=category.parent_id, product_cb_factory=lambda p: f"product:{p.id}",
cat_id=category_id, back_cb=f"cat:{category.parent_id}",
page=page_id, prev_cb=(f"products:{category_id}:{page_id - 1}" if page_id > 0 else None),
show_prev=page_id > 0, next_cb=(
show_next=products_len > PAGE_SIZE, f"products:{category_id}:{page_id + 1}"
if products_len > PAGE_SIZE
else None
),
columns=2,
), ),
) )
@@ -68,9 +73,18 @@ async def product_card(
product_id = int(cb.data.split(":")[1]) product_id = int(cb.data.split(":")[1])
product = await products_repo.get_product_by_id(session, product_id) product = await products_repo.get_product_by_id(session, product_id)
order_item = await order_service.get_cart_product_amount( if cb.from_user.id in VERIFIED_ACCOUNTS:
session, customer=cb.from_user.id, product_id=product_id kb = admins.edit_product(product_id=product.id, category_id=product.category_id)
) else:
order_item = await order_service.get_cart_product_amount(
session, customer=cb.from_user.id, product_id=product_id
)
kb = render_product_interactions(
product_id=product.id,
category_id=product.category_id,
show_cart=bool(order_item),
cart_amount=order_item,
)
if product.file_id: if product.file_id:
await cb.message.delete() await cb.message.delete()
@@ -78,24 +92,14 @@ async def product_card(
msg = await cb.message.answer_photo( msg = await cb.message.answer_photo(
product.file_id, product.file_id,
caption=get_product_description(product), caption=get_product_description(product),
reply_markup=render_product_interactions( reply_markup=kb,
product_id=product.id,
category_id=product.category_id,
show_cart=bool(order_item),
cart_amount=order_item,
),
) )
return return
if not os.path.isfile(product.img_path): if not os.path.isfile(product.img_path):
await cb.message.edit_text( await cb.message.edit_text(
get_product_description(product), get_product_description(product),
reply_markup=render_product_interactions( reply_markup=kb,
product_id=product.id,
category_id=product.category_id,
show_cart=bool(order_item),
cart_amount=order_item,
),
) )
return return
@@ -103,12 +107,7 @@ async def product_card(
msg = await cb.message.answer_photo( msg = await cb.message.answer_photo(
FSInputFile(product.img_path), FSInputFile(product.img_path),
caption=get_product_description(product), caption=get_product_description(product),
reply_markup=render_product_interactions( reply_markup=kb,
product_id=product.id,
category_id=product.category_id,
show_cart=bool(order_item),
cart_amount=order_item,
),
) )
if not msg.photo: if not msg.photo:
@@ -120,7 +119,7 @@ async def product_card(
@router.callback_query(F.data.startswith("cart_action:")) @router.callback_query(F.data.startswith("cart_action:"))
async def add_to_cart( async def cart_action(
cb: CallbackQuery, cb: CallbackQuery,
state: FSMContext, state: FSMContext,
session: AsyncSession, session: AsyncSession,

58
handlers/client/search.py Normal file
View File

@@ -0,0 +1,58 @@
from typing import Optional
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
from misc.kb.client import render_products
from misc.kb.common import return_menu
from misc.states import SearchStorage
from repositories.products import ProductRepository
router = Router()
@router.callback_query(F.data == "find")
async def search_trigger(cb: CallbackQuery, state: FSMContext):
await state.clear()
await cb.message.edit_text("🔍 Введите запрос:", reply_markup=return_menu)
await state.set_state(SearchStorage.query)
await state.set_data({"msg": cb.message})
@router.message(SearchStorage.query)
async def searching(
msg: Message,
state: FSMContext,
session: AsyncSession,
products_repo: ProductRepository,
):
data = await state.get_data()
await state.clear()
orig_msg: Optional[Message] = data.get("msg")
results = await products_repo.search(
session,
msg.text,
)
if not orig_msg:
await msg.answer(
"here",
reply_markup=render_products(
results,
product_cb_factory=lambda p: f"product:{p.id}",
back_cb="menu:main",
next_cb="",
),
)
return
await orig_msg.edit_text(
"here",
reply_markup=render_products(
results, product_cb_factory=lambda p: f"product:{p.id}", back_cb="menu:main"
),
)

View File

@@ -1,12 +1,26 @@
from typing import Optional from typing import Optional, 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
placeholder_kb = InlineKeyboardBuilder( main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
[[InlineKeyboardButton(text="🍃", callback_data="...")]] [
[InlineKeyboardButton(text="📦 Изменить товары", callback_data="cat:root")],
[
InlineKeyboardButton(
text="📜 Изменить категории", callback_data="edit:categories"
)
],
# [InlineKeyboardButton(text="", callback_data="")],
]
).as_markup() ).as_markup()
def back_to_product_kb(product_id: int, *, cb_factory: Callable[[int], str]):
return InlineKeyboardBuilder(
[[InlineKeyboardButton(text="⬅️", callback_data=cb_factory(product_id))]]
).as_markup()
def customer_contacts( def customer_contacts(
user_id: int, has_mention: bool, mention: Optional[str] = None user_id: int, has_mention: bool, mention: Optional[str] = None
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
@@ -44,3 +58,26 @@ def verify_payment(invoice_id: int):
] ]
] ]
).as_markup() ).as_markup()
def edit_product(product_id: int, category_id: int):
prefix = f"edit_product:{product_id}"
return InlineKeyboardBuilder(
[
[
InlineKeyboardButton(
text="✍️ Название", callback_data=f"{prefix}:name"
),
InlineKeyboardButton(
text="✍️ Описание",
callback_data=f"{prefix}:description",
),
],
[InlineKeyboardButton(text="💸 Цена", callback_data=f"{prefix}:price")],
[
InlineKeyboardButton(text="👁️", callback_data=f"{prefix}:hide"),
InlineKeyboardButton(text="", callback_data=f"{prefix}:delete"),
],
[InlineKeyboardButton(text="⬅️", callback_data=f"cat:{category_id}")],
]
).as_markup()

View File

@@ -1,4 +1,4 @@
from typing import Optional from typing import Optional, 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
@@ -6,10 +6,6 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from db.models import Category, Product from db.models import Category, Product
from dto.cart import CartItemDTO from dto.cart import CartItemDTO
return_menu = InlineKeyboardBuilder(
[[InlineKeyboardButton(text="⬅️ В меню", callback_data="menu:main")]]
).as_markup()
def pagination_row( def pagination_row(
*, *,
@@ -61,11 +57,13 @@ def render_category(
children: list[Category], children: list[Category],
parent_id: Optional[int] = None, parent_id: Optional[int] = None,
show_menu: bool = False, show_menu: bool = False,
*,
category_cb_factory: Callable[[Category], str],
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
btns = [ btns = [
InlineKeyboardButton( InlineKeyboardButton(
text=category.name, text=category.name,
callback_data=f"cat:{category.id}", callback_data=category_cb_factory(category),
) )
for category in children for category in children
] ]
@@ -88,35 +86,37 @@ def render_category(
def render_products( def render_products(
products: list[Product], products: list[Product],
*, *,
parent_id: int, product_cb_factory: Callable[[Product], str],
cat_id: int, back_cb: Optional[str] = None,
page: int = 0, prev_cb: Optional[str] = None,
show_prev: Optional[bool] = False, next_cb: Optional[str] = None,
show_next: Optional[bool] = False, columns: int = 2,
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
btns = [ builder = InlineKeyboardBuilder()
InlineKeyboardButton(text=product.name, callback_data=f"product:{product.id}")
for product in products
]
nav = pagination_row( if back_cb:
page=page, builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=back_cb))
prev_cb=f"products:{cat_id}:{page - 1}" if show_prev else None,
next_cb=f"products:{cat_id}:{page + 1}" if show_next else None,
)
return InlineKeyboardBuilder( for i in range(0, len(products), columns):
[ row = [
[ InlineKeyboardButton(
InlineKeyboardButton( text=p.name,
text="⬅️ Назад", callback_data=product_cb_factory(p),
callback_data=f"cat:{parent_id}", )
) for p in products[i : i + columns]
]
] ]
+ [btns[i : i + 2] for i in range(0, len(products), 2)] builder.row(*row)
+ [nav]
).as_markup() nav_buttons = []
if prev_cb:
nav_buttons.append(InlineKeyboardButton(text="◀️", callback_data=prev_cb))
if next_cb:
nav_buttons.append(InlineKeyboardButton(text="▶️", callback_data=next_cb))
if nav_buttons:
builder.row(*nav_buttons)
return builder.as_markup()
def render_product_interactions( def render_product_interactions(
@@ -165,6 +165,7 @@ def render_cart(
page: int = 0, page: int = 0,
show_prev: Optional[bool] = False, show_prev: Optional[bool] = False,
show_next: Optional[bool] = False, show_next: Optional[bool] = False,
show_purchase: Optional[bool] = False,
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
btns = [ btns = [
InlineKeyboardButton( InlineKeyboardButton(
@@ -180,19 +181,25 @@ def render_cart(
next_cb=f"cart:{page + 1}" if show_next else None, next_cb=f"cart:{page + 1}" if show_next else None,
) )
return InlineKeyboardBuilder( markup: list[list[InlineKeyboardButton]] = (
[ [
[ [
InlineKeyboardButton( InlineKeyboardButton(
text="⬅️ Назад", text="⬅️ Назад",
callback_data="menu:main", callback_data="menu:main",
) )
] ],
] ]
+ [btns[i : i + 2] for i in range(0, len(cart_items), 2)] + [btns[i : i + 2] for i in range(0, len(cart_items), 2)]
+ [nav] + [nav]
+ [[InlineKeyboardButton(text="🔷 Оформить заказ", callback_data="checkout")]] )
).as_markup()
if show_purchase:
markup.append(
[InlineKeyboardButton(text="🔷 Оформить заказ", callback_data="checkout")]
)
return InlineKeyboardBuilder(markup).as_markup()
def order_confirmation(order_id: int) -> InlineKeyboardMarkup: def order_confirmation(order_id: int) -> InlineKeyboardMarkup:

10
misc/kb/common.py Normal file
View File

@@ -0,0 +1,10 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
placeholder_kb: InlineKeyboardMarkup = InlineKeyboardBuilder(
[[InlineKeyboardButton(text="🍃", callback_data="...")]]
).as_markup()
return_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
[[InlineKeyboardButton(text="⬅️ В меню", callback_data="menu:main")]]
).as_markup()

View File

@@ -10,3 +10,11 @@ class CheckoutStorage(StatesGroup):
class PaymentStorage(StatesGroup): class PaymentStorage(StatesGroup):
refresh = State() refresh = State()
class SearchStorage(StatesGroup):
query = State()
class AdminControlStorage(StatesGroup):
edit_product = State()

View File

@@ -1,11 +1,17 @@
from db.models import Product from db.models import Product
from dto.cart import CartDTO from dto.cart import CartDTO
product_editing_mapping = {
"name": "новое название",
"description": "новое описание",
"price": "новую цену",
}
def get_product_description(product: Product) -> str: def get_product_description(product: Product) -> str:
output = f"📦 <b>{product.name}</b>\n\n<b><i>🏷️ {product.price}₽</i></b>" output = f"📦 <b>{product.name}</b>\n\n<b>🏷️ {product.price}₽</b>"
if product.description: if product.description:
output += f"\n\n📜 {product.description}" output += f"\n\n📜 <b><i>{product.description}</i></b>"
return output return output

View File

@@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from sqlalchemy import select, update from sqlalchemy import select
from db.models import Invoice from db.models import Invoice
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -37,19 +37,15 @@ class InvoiceRepository:
async def update_invoice_msg( async def update_invoice_msg(
self, session: AsyncSession, *, invoice_id: int, inline_message_id: int self, session: AsyncSession, *, invoice_id: int, inline_message_id: int
): ):
stmt = ( invoice = await self.get_invoice_by_id(session, invoice_id)
update(Invoice.__table__) invoice.inline_message_id = inline_message_id
.where(Invoice.id == invoice_id)
.values(inline_message_id=inline_message_id) await session.commit()
)
await session.execute(stmt)
async def update_invoice_status( async def update_invoice_status(
self, session: AsyncSession, *, invoice_id: int, status: InvoiceStatus self, session: AsyncSession, *, invoice_id: int, status: InvoiceStatus
): ):
stmt = ( invoice = await self.get_invoice_by_id(session, invoice_id)
update(Invoice.__table__) invoice.status = status
.where(Invoice.id == invoice_id)
.values(status=status) await session.commit()
)
await session.execute(stmt)

View File

@@ -1,5 +1,5 @@
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import delete, func, select, update from sqlalchemy import delete, func, select
from typing import Optional from typing import Optional
from db.models.orders import Order, OrderItem, OrderStatus from db.models.orders import Order, OrderItem, OrderStatus
@@ -55,13 +55,9 @@ class OrderItemRepository:
async def update_order_item_quantity( async def update_order_item_quantity(
self, session: AsyncSession, order_item_id: int, *, quantity: int = 0 self, session: AsyncSession, order_item_id: int, *, quantity: int = 0
): ):
stmt = ( item = await self.get_item_by_id(session, order_item_id)
update(OrderItem.__table__) item.quantity = quantity
.where(OrderItem.id == order_item_id)
.values(quantity=quantity)
)
await session.execute(stmt)
await session.commit() await session.commit()
async def create_order_item( async def create_order_item(
@@ -82,7 +78,7 @@ class OrderItemRepository:
return order_item return order_item
async def delete_item(self, session: AsyncSession, item_id: int): async def delete_item(self, session: AsyncSession, item_id: int):
stmt = delete(OrderItem.__table__).where(OrderItem.id == item_id) stmt = delete(OrderItem).where(OrderItem.id == item_id)
await session.execute(stmt) await session.execute(stmt)
await session.commit() await session.commit()

View File

@@ -48,13 +48,21 @@ class OrderRepository:
return order return order
async def remove_order_by_id(self, session: AsyncSession, order_id: int): async def remove_order_by_id(self, session: AsyncSession, order_id: int):
stmt = delete(Order.__table__).where(Order.id == order_id) stmt = delete(Order).where(Order.id == order_id)
await session.execute(stmt) await session.execute(stmt)
await session.commit() await session.commit()
async def remove_order_by_customer(self, session: AsyncSession, customer: int): async def remove_order_by_customer(self, session: AsyncSession, customer: int):
stmt = delete(Order.__table__).where(Order.customer == customer) stmt = delete(Order).where(Order.customer == customer)
await session.execute(stmt) await session.execute(stmt)
await session.commit() await session.commit()
async def update_order_status(
self, session: AsyncSession, order_id: int, status: OrderStatus
):
order = await self.get_order_by_id(session, order_id)
order.status = status
await session.commit()

View File

@@ -1,7 +1,9 @@
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update from sqlalchemy import select, update
from sqlalchemy.sql.expression import func
from typing import Optional from typing import Optional
from config import PAGE_SIZE
from db.models import Product from db.models import Product
@@ -31,10 +33,59 @@ class ProductRepository:
self, session: AsyncSession, product_id: int, *, file_id: str self, session: AsyncSession, product_id: int, *, file_id: str
) -> None: ) -> None:
stmt = ( stmt = (
update(Product.__table__) update(Product)
.where(Product.id == product_id) .where(Product.id == product_id)
.where(Product.file_id is None) .where(Product.file_id is None)
.values(file_id=file_id) .values(file_id=file_id)
) )
await session.execute(stmt) await session.execute(stmt)
await session.commit() await session.commit()
async def search(
self,
session: AsyncSession,
query: str,
*,
limit: int = PAGE_SIZE + 1,
offset: int = 0,
) -> list[Optional[Product]]:
ts_query = func.plainto_tsquery("simple", query)
stmt = (
select(Product)
.where(Product.search_vector.op("@@")(ts_query))
.order_by(func.ts_rank(Product.search_vector, ts_query).desc())
.limit(limit)
.offset(offset)
)
res = await session.scalars(stmt)
return list(res)
async def update_product_name_by_id(
self, session: AsyncSession, product_id: int, name: str
):
product = await self.get_product_by_id(session, product_id=product_id)
product.name = name
await session.commit()
return product
async def update_product_description_by_id(
self, session: AsyncSession, product_id: int, description: str
):
product = await self.get_product_by_id(session, product_id=product_id)
product.description = description
await session.commit()
return product
async def update_product_price_by_id(
self, session: AsyncSession, product_id: int, price: str
):
product = await self.get_product_by_id(session, product_id=product_id)
product.price = price
await session.commit()
return product

View File

@@ -52,7 +52,7 @@ class OrderService:
session, order_item.id, quantity=order_item.quantity + quantity session, order_item.id, quantity=order_item.quantity + quantity
) )
return order_item.quantity + quantity return order_item.quantity
async def remove_from_cart( async def remove_from_cart(
self, self,
@@ -162,7 +162,9 @@ class OrderService:
return CartDTO(items=cart_items, total=total, order_id=order.id) return CartDTO(items=cart_items, total=total, order_id=order.id)
async def build_full_cart_dto(self, session: AsyncSession, customer: int): async def build_full_cart_dto(
self, session: AsyncSession, customer: int
) -> CartDTO:
order = await self.order_repo.get_draft_order_by_user(session, customer) order = await self.order_repo.get_draft_order_by_user(session, customer)
cart_items = [] cart_items = []