diff --git a/dto/control.py b/dto/control.py
new file mode 100644
index 0000000..5552355
--- /dev/null
+++ b/dto/control.py
@@ -0,0 +1,10 @@
+from dataclasses import dataclass
+
+from aiogram.types import Message
+
+
+@dataclass
+class EditProductContext:
+ product_id: int
+ mode: str
+ msg: Message
diff --git a/handlers/admins/__init__.py b/handlers/admins/__init__.py
index df4f58b..5e7a61d 100644
--- a/handlers/admins/__init__.py
+++ b/handlers/admins/__init__.py
@@ -1,3 +1,5 @@
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]
diff --git a/handlers/admins/inline_mode.py b/handlers/admins/inline_mode.py
index ca88765..3549bdd 100644
--- a/handlers/admins/inline_mode.py
+++ b/handlers/admins/inline_mode.py
@@ -11,7 +11,8 @@ from aiogram.utils.deep_linking import create_start_link
from sqlalchemy.ext.asyncio import AsyncSession
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 repositories.invoices import InvoiceRepository
diff --git a/handlers/admins/menu.py b/handlers/admins/menu.py
new file mode 100644
index 0000000..e178d4e
--- /dev/null
+++ b/handlers/admins/menu.py
@@ -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)
diff --git a/handlers/admins/product_mgmt.py b/handlers/admins/product_mgmt.py
new file mode 100644
index 0000000..73c9db0
--- /dev/null
+++ b/handlers/admins/product_mgmt.py
@@ -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"✍️ Введите {product_editing_mapping.get(mode)}",
+ 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"✍️ Введите {product_editing_mapping.get(mode)}",
+ 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)
diff --git a/handlers/client/__init__.py b/handlers/client/__init__.py
index 7cc4294..71b8f95 100644
--- a/handlers/client/__init__.py
+++ b/handlers/client/__init__.py
@@ -5,6 +5,7 @@ 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
+from .search import router as search_router
routers = [
invoice_router,
@@ -14,4 +15,5 @@ routers = [
cart_router,
checkout_router,
security_router,
+ search_router,
]
diff --git a/handlers/client/cart.py b/handlers/client/cart.py
index 66e50fa..70a776c 100644
--- a/handlers/client/cart.py
+++ b/handlers/client/cart.py
@@ -5,9 +5,8 @@ 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 misc.kb.client import render_cart
+from misc.kb.common import return_menu
from services.orders import OrderService
router = Router()
@@ -25,11 +24,16 @@ async def cart_init(
cart = await order_service.build_pagination_cart_dto(
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(
- f"total: {cart.total}₽",
+ f"total: {cart.total}₽ | {len(cart.items)}",
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
)
+ if not len(cart.items):
+ await msg.answer("🛒 Корзина пуста.", reply_markup=return_menu)
+ return
+
await msg.answer(
f"total: {cart.total}₽",
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)),
),
)
diff --git a/handlers/client/catalogue.py b/handlers/client/catalogue.py
index 29ea8dc..2befea1 100644
--- a/handlers/client/catalogue.py
+++ b/handlers/client/catalogue.py
@@ -29,7 +29,9 @@ async def subcatalogue(
if cat_id == "root":
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(
"category selection",
reply_markup=kb,
@@ -61,11 +63,14 @@ async def subcatalogue(
"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,
+ product_cb_factory=lambda p: f"product:{p.id}",
+ back_cb=f"cat:{parent_id}",
+ next_cb=(
+ f"products:{cat_id}:{0 + 1}"
+ if products_len > PAGE_SIZE
+ else None
+ ),
+ columns=2,
),
)
return
@@ -74,16 +79,20 @@ async def subcatalogue(
"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,
+ product_cb_factory=lambda p: f"product:{p.id}",
+ back_cb=f"cat:{parent_id}",
+ prev_cb=f"products:{cat_id}:{0 - 1}" if products_len < 0 else None,
+ next_cb=(
+ f"products:{cat_id}:{0 + 1}" if products_len > PAGE_SIZE else None
+ ),
+ columns=2,
),
)
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(
"category selection",
diff --git a/handlers/client/checkout.py b/handlers/client/checkout.py
index 0907cf5..24dfcbe 100644
--- a/handlers/client/checkout.py
+++ b/handlers/client/checkout.py
@@ -6,13 +6,11 @@ from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from config import 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,
- return_menu,
-)
+from misc.kb.client import order_confirmation, order_specs_confirmation
+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
@@ -152,13 +150,6 @@ async def payment_create(
await state.clear()
try:
- await cb.message.edit_text(
- "🟢 Ваш заказ успешно отправлен!\n"
- "📦 Мы свяжемся с вами для уточнения деталей в ближайшее время.\n\n"
- "‼️ Чтобы убедиться, что вы общаетесь с доверенным контактом ШВЕЙТЕХЦЕНТР, перешлите любое сообщение человека в этот чат.",
- reply_markup=return_menu,
- )
-
cart = await order_service.build_full_cart_dto(session, cb.from_user.id)
await cb.bot.send_message(
NOTIFICATION_CHANNEL,
@@ -173,7 +164,17 @@ async def payment_create(
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(
+ "🟢 Ваш заказ успешно отправлен!\n"
+ "📦 Мы свяжемся с вами для уточнения деталей в ближайшее время.\n\n"
+ "‼️ Чтобы убедиться, что вы общаетесь с доверенным контактом ШВЕЙТЕХЦЕНТР, перешлите любое сообщение человека в этот чат.",
+ reply_markup=return_menu,
+ )
+
except Exception as e:
await cb.message.edit_text(
"🍃 Что-то пошло не так, повторите попытку позже...",
diff --git a/handlers/client/invoices.py b/handlers/client/invoices.py
index d29c134..b60a937 100644
--- a/handlers/client/invoices.py
+++ b/handlers/client/invoices.py
@@ -11,7 +11,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from db.models.invoices import InvoiceStatus
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 config import card_info
diff --git a/handlers/client/menu.py b/handlers/client/menu.py
index b0425fc..836d110 100644
--- a/handlers/client/menu.py
+++ b/handlers/client/menu.py
@@ -12,7 +12,7 @@ router = Router()
@router.message(Command("start"))
-async def main_menu(
+async def user_main_menu(
msg: Message,
state: FSMContext,
session: AsyncSession,
@@ -29,7 +29,7 @@ async def main_menu(
@router.callback_query(F.data.startswith("menu:"))
-async def main_menu_cb(
+async def user_main_menu_cb(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
diff --git a/handlers/client/products.py b/handlers/client/products.py
index e06514d..ca0eb5b 100644
--- a/handlers/client/products.py
+++ b/handlers/client/products.py
@@ -5,7 +5,8 @@ from aiogram.types import CallbackQuery, FSInputFile
from aiogram.fsm.context import FSMContext
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 (
render_product_interactions,
render_products,
@@ -46,11 +47,15 @@ async def products_pagination(
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,
+ product_cb_factory=lambda p: f"product:{p.id}",
+ back_cb=f"cat:{category.parent_id}",
+ prev_cb=(f"products:{category_id}:{page_id - 1}" if page_id > 0 else None),
+ next_cb=(
+ 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 = 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 cb.from_user.id in VERIFIED_ACCOUNTS:
+ 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:
await cb.message.delete()
@@ -78,24 +92,14 @@ async def product_card(
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,
- ),
+ reply_markup=kb,
)
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,
- ),
+ reply_markup=kb,
)
return
@@ -103,12 +107,7 @@ async def product_card(
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,
- ),
+ reply_markup=kb,
)
if not msg.photo:
@@ -120,7 +119,7 @@ async def product_card(
@router.callback_query(F.data.startswith("cart_action:"))
-async def add_to_cart(
+async def cart_action(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
diff --git a/handlers/client/search.py b/handlers/client/search.py
new file mode 100644
index 0000000..f864a91
--- /dev/null
+++ b/handlers/client/search.py
@@ -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"
+ ),
+ )
diff --git a/misc/kb/admins.py b/misc/kb/admins.py
index d70db04..6290adb 100644
--- a/misc/kb/admins.py
+++ b/misc/kb/admins.py
@@ -1,12 +1,26 @@
-from typing import Optional
+from typing import Optional, Callable
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
-placeholder_kb = InlineKeyboardBuilder(
- [[InlineKeyboardButton(text="🍃", callback_data="...")]]
+main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
+ [
+ [InlineKeyboardButton(text="📦 Изменить товары", callback_data="cat:root")],
+ [
+ InlineKeyboardButton(
+ text="📜 Изменить категории", callback_data="edit:categories"
+ )
+ ],
+ # [InlineKeyboardButton(text="", callback_data="")],
+ ]
).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(
user_id: int, has_mention: bool, mention: Optional[str] = None
) -> InlineKeyboardMarkup:
@@ -44,3 +58,26 @@ def verify_payment(invoice_id: int):
]
]
).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()
diff --git a/misc/kb/client.py b/misc/kb/client.py
index 6f1e502..ee143bf 100644
--- a/misc/kb/client.py
+++ b/misc/kb/client.py
@@ -1,4 +1,4 @@
-from typing import Optional
+from typing import Optional, Callable
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -6,10 +6,6 @@ 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(
*,
@@ -61,11 +57,13 @@ def render_category(
children: list[Category],
parent_id: Optional[int] = None,
show_menu: bool = False,
+ *,
+ category_cb_factory: Callable[[Category], str],
) -> InlineKeyboardMarkup:
btns = [
InlineKeyboardButton(
text=category.name,
- callback_data=f"cat:{category.id}",
+ callback_data=category_cb_factory(category),
)
for category in children
]
@@ -88,35 +86,37 @@ def render_category(
def render_products(
products: list[Product],
*,
- parent_id: int,
- cat_id: int,
- page: int = 0,
- show_prev: Optional[bool] = False,
- show_next: Optional[bool] = False,
+ product_cb_factory: Callable[[Product], str],
+ back_cb: Optional[str] = None,
+ prev_cb: Optional[str] = None,
+ next_cb: Optional[str] = None,
+ columns: int = 2,
) -> InlineKeyboardMarkup:
- btns = [
- InlineKeyboardButton(text=product.name, callback_data=f"product:{product.id}")
- for product in products
- ]
+ builder = InlineKeyboardBuilder()
- nav = pagination_row(
- page=page,
- 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,
- )
+ if back_cb:
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=back_cb))
- return InlineKeyboardBuilder(
- [
- [
- InlineKeyboardButton(
- text="⬅️ Назад",
- callback_data=f"cat:{parent_id}",
- )
- ]
+ for i in range(0, len(products), columns):
+ row = [
+ InlineKeyboardButton(
+ text=p.name,
+ callback_data=product_cb_factory(p),
+ )
+ for p in products[i : i + columns]
]
- + [btns[i : i + 2] for i in range(0, len(products), 2)]
- + [nav]
- ).as_markup()
+ builder.row(*row)
+
+ 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(
@@ -165,6 +165,7 @@ def render_cart(
page: int = 0,
show_prev: Optional[bool] = False,
show_next: Optional[bool] = False,
+ show_purchase: Optional[bool] = False,
) -> InlineKeyboardMarkup:
btns = [
InlineKeyboardButton(
@@ -180,19 +181,25 @@ def render_cart(
next_cb=f"cart:{page + 1}" if show_next else None,
)
- return InlineKeyboardBuilder(
+ markup: list[list[InlineKeyboardButton]] = (
[
[
InlineKeyboardButton(
text="⬅️ Назад",
callback_data="menu:main",
)
- ]
+ ],
]
+ [btns[i : i + 2] for i in range(0, len(cart_items), 2)]
+ [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:
diff --git a/misc/kb/common.py b/misc/kb/common.py
new file mode 100644
index 0000000..fb147ee
--- /dev/null
+++ b/misc/kb/common.py
@@ -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()
diff --git a/misc/states.py b/misc/states.py
index 6d86e95..b5c1eac 100644
--- a/misc/states.py
+++ b/misc/states.py
@@ -10,3 +10,11 @@ class CheckoutStorage(StatesGroup):
class PaymentStorage(StatesGroup):
refresh = State()
+
+
+class SearchStorage(StatesGroup):
+ query = State()
+
+
+class AdminControlStorage(StatesGroup):
+ edit_product = State()
diff --git a/misc/texts.py b/misc/texts.py
index d4f9417..07be491 100644
--- a/misc/texts.py
+++ b/misc/texts.py
@@ -1,11 +1,17 @@
from db.models import Product
from dto.cart import CartDTO
+product_editing_mapping = {
+ "name": "новое название",
+ "description": "новое описание",
+ "price": "новую цену",
+}
+
def get_product_description(product: Product) -> str:
- output = f"📦 {product.name}\n\n🏷️ {product.price}₽"
+ output = f"📦 {product.name}\n\n🏷️ {product.price}₽"
if product.description:
- output += f"\n\n📜 {product.description}"
+ output += f"\n\n📜 {product.description}"
return output
diff --git a/repositories/invoices.py b/repositories/invoices.py
index bb3afac..3a2d20c 100644
--- a/repositories/invoices.py
+++ b/repositories/invoices.py
@@ -1,6 +1,6 @@
from typing import Optional
-from sqlalchemy import select, update
+from sqlalchemy import select
from db.models import Invoice
from sqlalchemy.ext.asyncio import AsyncSession
@@ -37,19 +37,15 @@ class InvoiceRepository:
async def update_invoice_msg(
self, session: AsyncSession, *, invoice_id: int, inline_message_id: int
):
- stmt = (
- update(Invoice.__table__)
- .where(Invoice.id == invoice_id)
- .values(inline_message_id=inline_message_id)
- )
- await session.execute(stmt)
+ invoice = await self.get_invoice_by_id(session, invoice_id)
+ invoice.inline_message_id = inline_message_id
+
+ await session.commit()
async def update_invoice_status(
self, session: AsyncSession, *, invoice_id: int, status: InvoiceStatus
):
- stmt = (
- update(Invoice.__table__)
- .where(Invoice.id == invoice_id)
- .values(status=status)
- )
- await session.execute(stmt)
+ invoice = await self.get_invoice_by_id(session, invoice_id)
+ invoice.status = status
+
+ await session.commit()
diff --git a/repositories/order_items.py b/repositories/order_items.py
index 3f987af..2930e1c 100644
--- a/repositories/order_items.py
+++ b/repositories/order_items.py
@@ -1,5 +1,5 @@
from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import delete, func, select, update
+from sqlalchemy import delete, func, select
from typing import Optional
from db.models.orders import Order, OrderItem, OrderStatus
@@ -55,13 +55,9 @@ class OrderItemRepository:
async def update_order_item_quantity(
self, session: AsyncSession, order_item_id: int, *, quantity: int = 0
):
- stmt = (
- update(OrderItem.__table__)
- .where(OrderItem.id == order_item_id)
- .values(quantity=quantity)
- )
+ item = await self.get_item_by_id(session, order_item_id)
+ item.quantity = quantity
- await session.execute(stmt)
await session.commit()
async def create_order_item(
@@ -82,7 +78,7 @@ class OrderItemRepository:
return order_item
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.commit()
diff --git a/repositories/orders.py b/repositories/orders.py
index 286bacc..f3d8bdb 100644
--- a/repositories/orders.py
+++ b/repositories/orders.py
@@ -48,13 +48,21 @@ class OrderRepository:
return order
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.commit()
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.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()
diff --git a/repositories/products.py b/repositories/products.py
index 6da6d05..9efaf34 100644
--- a/repositories/products.py
+++ b/repositories/products.py
@@ -1,7 +1,9 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
+from sqlalchemy.sql.expression import func
from typing import Optional
+from config import PAGE_SIZE
from db.models import Product
@@ -31,10 +33,59 @@ class ProductRepository:
self, session: AsyncSession, product_id: int, *, file_id: str
) -> None:
stmt = (
- update(Product.__table__)
+ update(Product)
.where(Product.id == product_id)
.where(Product.file_id is None)
.values(file_id=file_id)
)
await session.execute(stmt)
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
diff --git a/services/orders.py b/services/orders.py
index 90a4b08..729c77f 100644
--- a/services/orders.py
+++ b/services/orders.py
@@ -52,7 +52,7 @@ class OrderService:
session, order_item.id, quantity=order_item.quantity + quantity
)
- return order_item.quantity + quantity
+ return order_item.quantity
async def remove_from_cart(
self,
@@ -162,7 +162,9 @@ class OrderService:
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)
cart_items = []