chore: minor fixes and formatting

This commit is contained in:
2026-08-12 19:52:30 +07:00
parent a011b2724b
commit ca276cdd61
48 changed files with 394 additions and 354 deletions

1
.gitignore vendored
View File

@@ -136,6 +136,7 @@ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
*.env
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject

View File

@@ -86,7 +86,7 @@ path_separator = os
# database URL. This is consumed by the user-maintained env.py script only. # database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py # other means of configuring database URLs may be customized within the env.py
# file. # file.
sqlalchemy.url = postgresql+asyncpg://postgres:HEXDEVFUCKINGSUCKS!@localhost:5432/postgres sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/postgres
[post_write_hooks] [post_write_hooks]

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
import os
from logging.config import fileConfig from logging.config import fileConfig
from sqlalchemy import pool from sqlalchemy import pool
@@ -6,14 +7,17 @@ from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context from alembic import context
from db.base import Base from db.base import Base
from db.models import * # noqa: F403 from db.models import *
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
config = context.config config = context.config
postgres_url = os.getenv("POSTGRES_URL")
if postgres_url:
config.set_main_option("sqlalchemy.url", postgres_url)
# Interpret the config file for Python logging. # Interpret the config file for Python logging.
# This line sets up loggers basically. # This line sets up loggers basically.
if config.config_file_name is not None: if config.config_file_name is not None:

View File

@@ -1,6 +1,7 @@
from dataclasses import dataclass
import os
import logging import logging
import os
from dataclasses import dataclass
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -26,9 +27,10 @@ POSTGRES_URL = os.getenv(
card_info = CardInfo(os.getenv("CARD_NUMBER", ""), os.getenv("CARD_HOLDER", "")) card_info = CardInfo(os.getenv("CARD_NUMBER", ""), os.getenv("CARD_HOLDER", ""))
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/1") REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/1")
PROXY_URL = os.getenv("PROXY_URL")
### Constants ### ### Constants ###
PAGE_SIZE = 8 PAGE_SIZE = 8
VERIFIED_ACCOUNTS = [1026030711, 8480400744] VERIFIED_ACCOUNTS = [8480400744, 6551145249]
NOTIFICATION_CHANNEL = -1003849564110 NOTIFICATION_CHANNEL = -1003836761482

View File

@@ -1,4 +1,4 @@
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
from config import POSTGRES_URL from config import POSTGRES_URL

View File

@@ -1,13 +1,13 @@
from .orders import Order, OrderItem, OrderStatus
from .products import Product, Category
from .invoices import Invoice, InvoiceStatus from .invoices import Invoice, InvoiceStatus
from .orders import Order, OrderItem, OrderStatus
from .products import Category, Product
__all__ = [ __all__ = [
"Category",
"Invoice",
"InvoiceStatus",
"Order", "Order",
"OrderItem", "OrderItem",
"OrderStatus", "OrderStatus",
"Product", "Product",
"Category",
"Invoice",
"InvoiceStatus",
] ]

View File

@@ -1,8 +1,9 @@
from datetime import datetime from datetime import datetime
from enum import Enum as E_cls from enum import Enum as E_cls
from sqlalchemy import DateTime, func, Enum
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy import DateTime, Enum, func
from sqlalchemy.dialects.postgresql import BIGINT from sqlalchemy.dialects.postgresql import BIGINT
from sqlalchemy.orm import Mapped, mapped_column
from db.base import Base from db.base import Base

View File

@@ -1,8 +1,9 @@
import enum import enum
from typing import List, Optional
from sqlalchemy import ForeignKey, BigInteger from sqlalchemy import BigInteger, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import ENUM from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import Mapped, mapped_column, relationship
from db.base import Base from db.base import Base
from db.models.products import Product from db.models.products import Product
@@ -19,10 +20,10 @@ class Order(Base):
__tablename__ = "orders" __tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
customer: Mapped[Optional[int]] = mapped_column(BigInteger) customer: Mapped[int | None] = mapped_column(BigInteger)
status: Mapped[OrderStatus] = mapped_column(ENUM(OrderStatus, name="order_status")) status: Mapped[OrderStatus] = mapped_column(ENUM(OrderStatus, name="order_status"))
items: Mapped[List["OrderItem"]] = relationship("OrderItem", back_populates="order") items: Mapped[list["OrderItem"]] = relationship("OrderItem", back_populates="order")
class OrderItem(Base): class OrderItem(Base):

View File

@@ -1,7 +1,6 @@
from typing import Optional from sqlalchemy import Boolean, ForeignKey, Index, Text
from sqlalchemy import ForeignKey, Text, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import TSVECTOR from sqlalchemy.dialects.postgresql import TSVECTOR
from sqlalchemy.orm import Mapped, mapped_column, relationship
from db.base import Base from db.base import Base
@@ -11,7 +10,7 @@ class Category(Base):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(Text, nullable=False) name: Mapped[str] = mapped_column(Text, nullable=False)
parent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("categories.id")) parent_id: Mapped[int | None] = mapped_column(ForeignKey("categories.id"))
parent: Mapped["Category"] = relationship("Category", remote_side=[id]) parent: Mapped["Category"] = relationship("Category", remote_side=[id])
@@ -20,15 +19,14 @@ class Product(Base):
__tablename__ = "products" __tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
category_id: Mapped[Optional[int]] = mapped_column(ForeignKey("categories.id")) category_id: Mapped[int | None] = mapped_column(ForeignKey("categories.id"))
name: Mapped[str] = mapped_column(Text, nullable=False) name: Mapped[str] = mapped_column(Text, nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text) description: Mapped[str | None] = mapped_column(Text)
price: Mapped[int] = mapped_column() price: Mapped[int] = mapped_column()
img_path: Mapped[str] = mapped_column(Text, nullable=True) img_path: Mapped[str] = mapped_column(Text, nullable=True)
file_id: Mapped[str] = mapped_column(Text, nullable=True, unique=True) file_id: Mapped[str] = mapped_column(Text, nullable=True, unique=True)
is_hidden: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
search_vector: Mapped[str] = mapped_column(TSVECTOR) search_vector: Mapped[str] = mapped_column(TSVECTOR)
__table_args__ = ( __table_args__ = (Index("idx_products_search", "search_vector", postgresql_using="gin"),)
Index("idx_products_search", "search_vector", postgresql_using="gin"),
)

View File

@@ -1,12 +1,12 @@
from .cart import CartItemDTO from .cart import CartItemDTO
from .control import EditProductContext from .catalogue import CatalogueType, CatalogueView
from .checkout import CheckoutContext from .checkout import CheckoutContext
from .catalogue import CatalogueView, CatalogueType from .control import EditProductContext
__all__ = [ __all__ = [
"CartItemDTO", "CartItemDTO",
"EditProductContext",
"CheckoutContext",
"CatalogueView",
"CatalogueType", "CatalogueType",
"CatalogueView",
"CheckoutContext",
"EditProductContext",
] ]

View File

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

View File

@@ -1,6 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Optional, Union
from aiogram.types import Message from aiogram.types import Message
@@ -16,12 +15,12 @@ class CatalogueType(Enum):
@dataclass @dataclass
class CatalogueView: class CatalogueView:
view_type: Optional[CatalogueType] = None view_type: CatalogueType | None = None
category: Optional[Category] = None category: Category | None = None
children: Optional[list[Category]] = None children: list[Category] | None = None
products: Optional[list[Product]] = None products: list[Product] | None = None
page: int = 0 page: int = 0
parent_id: Union[int, str, None] = None parent_id: int | str | None = None
has_next: bool = False has_next: bool = False
show_menu: bool = False show_menu: bool = False
@@ -29,7 +28,7 @@ class CatalogueView:
@dataclass @dataclass
class NewCatalogueElement: class NewCatalogueElement:
msg: Message msg: Message
parent_id: Union[int, str] parent_id: int | str
@dataclass @dataclass

View File

@@ -1,5 +1,4 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
from aiogram.types import Message from aiogram.types import Message
@@ -8,6 +7,6 @@ from aiogram.types import Message
class CheckoutContext: class CheckoutContext:
orig_msg: Message orig_msg: Message
order_id: int order_id: int
name: Optional[str] = None name: str | None = None
phone: Optional[str] = None phone: str | None = None
address: Optional[str] = None address: str | None = None

View File

@@ -1,4 +1,4 @@
from .client import routers as client_routers
from .admins import routers as admins_routers from .admins import routers as admins_routers
from .client import routers as client_routers
__all__ = ["client_routers", "admins_routers"] __all__ = ["admins_routers", "client_routers"]

View File

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

View File

@@ -1,14 +1,14 @@
from aiogram import F, Router from aiogram import F, Router
from aiogram.types import CallbackQuery, Message
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from dto.catalogue import CategoryActionContext from dto.catalogue import CategoryActionContext
from misc.filters import IsVerified from misc.filters import IsVerified
from misc.kb.client import created_category, get_back_to_catalogue from misc.kb.client import created_category, get_back_to_catalogue
from misc.kb.common import return_menu
from misc.mapper import parse_cat_id from misc.mapper import parse_cat_id
from misc.states import AdminControlStorage from misc.states import AdminControlStorage
from misc.kb.common import return_menu
from repositories.categories import CategoriesRepository from repositories.categories import CategoriesRepository
from services.catalogue import CatalogueService from services.catalogue import CatalogueService
@@ -67,6 +67,6 @@ async def create_new_category(
await ctx.msg.edit_text( await ctx.msg.edit_text(
f"✅ Категория {msg.text} создана успешно.", f"✅ Категория {msg.text} создана успешно.",
reply_markup=created_category( reply_markup=created_category(
category.id, f"🛒 {category.name}", category.parent_id category.id, f"🛒 {category.name}", category.parent_id or "root"
), ),
) )

View File

@@ -1,6 +1,6 @@
import logging import logging
from aiogram import Router, F from aiogram import F, Router
from aiogram.types import ( from aiogram.types import (
ChosenInlineResult, ChosenInlineResult,
InlineQuery, InlineQuery,
@@ -26,9 +26,7 @@ async def send_invoice(iq: InlineQuery, amount: int):
InlineQueryResultArticle( InlineQueryResultArticle(
id=dict_to_b64({"a": amount}), id=dict_to_b64({"a": amount}),
title=f"💸 Счёт на {amount}₽", title=f"💸 Счёт на {amount}₽",
input_message_content=InputTextMessageContent( input_message_content=InputTextMessageContent(message_text="⏳ Создаю счёт..."),
message_text="⏳ Создаю счёт..."
),
reply_markup=placeholder_kb, reply_markup=placeholder_kb,
description="Нажмите, чтобы создать счёт.", description="Нажмите, чтобы создать счёт.",
), ),

View File

@@ -1,9 +1,9 @@
import logging import logging
from aiogram import Router, F from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from misc.filters import IsVerified from misc.filters import IsVerified
from misc.kb.admins import main_menu from misc.kb.admins import main_menu

View File

@@ -1,12 +1,14 @@
import logging import logging
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery from aiogram import F, Router
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from dto.catalogue import CategoryActionContext from dto.catalogue import CategoryActionContext
from dto.control import EditProductContext from dto.control import EditProductContext
from misc.filters import IsVerified from misc.filters import IsVerified
from misc.kb import admins
from misc.kb.admins import back_to_product_kb from misc.kb.admins import back_to_product_kb
from misc.kb.client import get_back_to_catalogue from misc.kb.client import get_back_to_catalogue
from misc.mapper import parse_cat_id from misc.mapper import parse_cat_id
@@ -19,7 +21,7 @@ logger = logging.getLogger(__name__)
router = Router() router = Router()
@router.callback_query(IsVerified(), F.data.startswith("edit_product:")) @router.callback_query(IsVerified(), F.data.regexp(r"^edit_product:\d+:(name|description|price)$"))
async def edit_product(cb: CallbackQuery, state: FSMContext): async def edit_product(cb: CallbackQuery, state: FSMContext):
await state.clear() await state.clear()
@@ -31,18 +33,14 @@ async def edit_product(cb: CallbackQuery, state: FSMContext):
await cb.message.delete() await cb.message.delete()
msg = await cb.message.answer( msg = await cb.message.answer(
f"<b>✍️ Введите {product_editing_mapping.get(mode)}</b>", f"<b>✍️ Введите {product_editing_mapping.get(mode)}</b>",
reply_markup=back_to_product_kb( reply_markup=back_to_product_kb(product_id, cb_factory=lambda p: f"product:{p}"),
product_id, cb_factory=lambda p: f"product:{p}"
),
) )
else: else:
msg = cb.message msg = cb.message
await cb.message.edit_text( await cb.message.edit_text(
f"<b>✍️ Введите {product_editing_mapping.get(mode)}</b>", f"<b>✍️ Введите {product_editing_mapping.get(mode)}</b>",
reply_markup=back_to_product_kb( reply_markup=back_to_product_kb(product_id, cb_factory=lambda p: f"product:{p}"),
product_id, cb_factory=lambda p: f"product:{p}"
),
) )
ctx = EditProductContext(int(product_id), mode, msg) ctx = EditProductContext(int(product_id), mode, msg)
@@ -68,13 +66,9 @@ async def edit_product_query(
try: try:
await ctx.msg.edit_text("⏳") await ctx.msg.edit_text("⏳")
if ctx.mode == "name": if ctx.mode == "name":
await products_repo.update_product_name_by_id( await products_repo.update_product_name_by_id(session, ctx.product_id, msg.text)
session, ctx.product_id, msg.text
)
elif ctx.mode == "description": elif ctx.mode == "description":
await products_repo.update_product_description_by_id( await products_repo.update_product_description_by_id(session, ctx.product_id, msg.text)
session, ctx.product_id, msg.text
)
elif ctx.mode == "price": elif ctx.mode == "price":
if msg.text.isdigit(): if msg.text.isdigit():
await products_repo.update_product_price_by_id( await products_repo.update_product_price_by_id(
@@ -99,6 +93,33 @@ async def edit_product_query(
await ctx.msg.edit_text(text="‼️ Что-то пошло не так.", reply_markup=kb) await ctx.msg.edit_text(text="‼️ Что-то пошло не так.", reply_markup=kb)
@router.callback_query(IsVerified(), F.data.startswith("edit_product:"), F.data.endswith(":hide"))
async def toggle_product_visibility(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
products_repo: ProductRepository,
):
await state.clear()
product_id = int(cb.data.split(":")[1])
product = await products_repo.toggle_product_hidden_by_id(session, product_id)
if not product:
await cb.answer("Товар не найден.", show_alert=True)
return
await cb.answer("Товар скрыт." if product.is_hidden else "Товар снова виден.")
if cb.message.photo:
await cb.message.edit_caption(
caption=cb.message.caption,
reply_markup=admins.edit_product(product),
)
return
await cb.message.edit_reply_markup(reply_markup=admins.edit_product(product))
@router.callback_query(F.data.startswith("edit:")) @router.callback_query(F.data.startswith("edit:"))
async def edit_entry(cb: CallbackQuery, state: FSMContext): async def edit_entry(cb: CallbackQuery, state: FSMContext):
await state.clear() await state.clear()
@@ -143,6 +164,4 @@ async def edit_category(
session=session, category_id=parse_cat_id(ctx.parent_id), value=msg.text session=session, category_id=parse_cat_id(ctx.parent_id), value=msg.text
) )
await ctx.msg.edit_text( await ctx.msg.edit_text("✅ Успешно.", reply_markup=get_back_to_catalogue(cat.parent_id))
"✅ Успешно.", reply_markup=get_back_to_catalogue(cat.parent_id)
)

View File

@@ -1,11 +1,11 @@
from .cart import router as cart_router
from .catalogue import router as catalogue_router
from .checkout import router as checkout_router
from .invoices import router as invoice_router from .invoices import router as invoice_router
from .menu import router as menu_router from .menu import router as menu_router
from .catalogue import router as catalogue_router
from .products import router as products_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
from .search import router as search_router from .search import router as search_router
from .security import router as security_router
routers = [ routers = [
invoice_router, invoice_router,

View File

@@ -1,7 +1,7 @@
from aiogram import Router, F from aiogram import F, Router
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import PAGE_SIZE from config import PAGE_SIZE
@@ -21,15 +21,14 @@ async def cart_init(
): ):
await state.clear() await state.clear()
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): if not len(cart.items):
await cb.message.edit_text("🛒 Корзина пуста.", reply_markup=return_menu) await cb.message.edit_text("🛒 Корзина пуста.", reply_markup=return_menu)
return return
await cb.message.edit_text( await cb.message.edit_text(
f"total: {cart.total}₽ | {len(cart.items)}", "<b>🌸 Убедитесь в том, что все желаемые позиции есть в заказе ниже!</b>\n\n"
f"<b>Сумма:</b> <i>{cart.total}₽</i>",
reply_markup=render_cart( reply_markup=render_cart(
cart.items[:PAGE_SIZE], cart.items[:PAGE_SIZE],
show_next=len(cart.items) > PAGE_SIZE, show_next=len(cart.items) > PAGE_SIZE,
@@ -47,9 +46,7 @@ async def cart_cmd(
): ):
await state.clear() await state.clear()
cart = await order_service.build_pagination_cart_dto( cart = await order_service.build_pagination_cart_dto(session, customer=msg.from_user.id)
session, customer=msg.from_user.id
)
if not len(cart.items): if not len(cart.items):
await msg.answer("🛒 Корзина пуста.", reply_markup=return_menu) await msg.answer("🛒 Корзина пуста.", reply_markup=return_menu)

View File

@@ -1,6 +1,6 @@
from aiogram import Router, F from aiogram import F, Router
from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import VERIFIED_ACCOUNTS from config import VERIFIED_ACCOUNTS
@@ -24,7 +24,7 @@ async def subcatalogue(
cat_id = cb.data.split(":")[1] cat_id = cb.data.split(":")[1]
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
view = await catalogue_service.build_category_view(session, cat_id) view = await catalogue_service.build_category_view(session, cat_id, include_hidden=is_admin)
kb = render_catalogue(view, is_admin=is_admin) kb = render_catalogue(view, is_admin=is_admin)
path = await catalogue_service.get_path(session, cat_id) path = await catalogue_service.get_path(session, cat_id)

View File

@@ -1,8 +1,8 @@
import logging import logging
from typing import Optional
from aiogram import Router, F from aiogram import F, Router
from aiogram.types import CallbackQuery, Message
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import NOTIFICATION_CHANNEL from config import NOTIFICATION_CHANNEL
@@ -98,7 +98,7 @@ async def checkout_phone(msg: Message, state: FSMContext):
ctx.phone = msg.text ctx.phone = msg.text
await ctx.orig_msg.edit_text( await ctx.orig_msg.edit_text(
f"<i>👤 Имя: {ctx.name}</i>\n<i>📱 Номер телефона: {ctx.phone}</i>\n\n<b>📍 Укажите ваш адрес (город, улица):</b>", f"<i>👤 Имя: {ctx.name}</i>\n<i>📱 Номер телефона: {ctx.phone}</i>\n\n<b>📍 Укажите адрес ближайшего ПВЗ Ozon (город, улица):</b>",
reply_markup=return_menu, reply_markup=return_menu,
) )
await state.set_state(CheckoutStorage.address) await state.set_state(CheckoutStorage.address)
@@ -119,7 +119,7 @@ async def checkout_address(msg: Message, state: FSMContext):
"━━━━━━━━━━━━━━\n\n" "━━━━━━━━━━━━━━\n\n"
f"<i>👤 Имя: {ctx.name}</i>\n" f"<i>👤 Имя: {ctx.name}</i>\n"
f"<i>📱 Номер телефона: {ctx.phone}</i>\n" f"<i>📱 Номер телефона: {ctx.phone}</i>\n"
f"<i>📍 Ваш адрес: {ctx.address}</i>\n\n" f"<i>📍 Адрес ближайшего ПВЗ Ozon: {ctx.address}</i>\n\n"
"━━━━━━━━━━━━━━", "━━━━━━━━━━━━━━",
reply_markup=order_specs_confirmation(ctx.order_id), reply_markup=order_specs_confirmation(ctx.order_id),
) )
@@ -138,7 +138,7 @@ async def payment_create(
order_repo: OrderRepository, order_repo: OrderRepository,
): ):
data = await state.get_data() data = await state.get_data()
ctx: Optional[CheckoutContext] = data.get("ctx") ctx: CheckoutContext | None = data.get("ctx")
if not ctx: if not ctx:
await cb.message.edit_text( await cb.message.edit_text(
"🍃 Что-то пошло не так, повторите попытку позже...", "🍃 Что-то пошло не так, повторите попытку позже...",
@@ -157,16 +157,14 @@ async def payment_create(
"━━━━━━━━━━━━━━\n\n" "━━━━━━━━━━━━━━\n\n"
f"<i>👤 Имя: {ctx.name}</i>\n" f"<i>👤 Имя: {ctx.name}</i>\n"
f"<i>📱 Номер телефона: {ctx.phone}</i>\n" f"<i>📱 Номер телефона: {ctx.phone}</i>\n"
f"<i>📍 Адрес: {ctx.address}</i>\n\n" f"<i>📍 Адрес ближайшего ПВЗ Ozon: {ctx.address}</i>\n\n"
"━━━━━━━━━━━━━━\n" "━━━━━━━━━━━━━━\n"
f"{get_order_item_list(cart)}", f"{get_order_item_list(cart)}",
reply_markup=customer_contacts( reply_markup=customer_contacts(
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.update_order_status( await order_repo.update_order_status(session, cart.order_id, OrderStatus.CREATED)
session, cart.order_id, OrderStatus.CREATED
)
await cb.message.edit_text( await cb.message.edit_text(
"<b>🟢 Ваш заказ успешно отправлен!</b>\n" "<b>🟢 Ваш заказ успешно отправлен!</b>\n"

View File

@@ -1,6 +1,6 @@
import logging import logging
from aiogram import Router, F from aiogram import F, Router
from aiogram.filters import CommandObject, CommandStart from aiogram.filters import CommandObject, CommandStart
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import ( from aiogram.types import (
@@ -9,12 +9,12 @@ from aiogram.types import (
) )
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import card_info
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 from misc.kb.client import confirm_payment
from misc.kb.common import return_menu from misc.kb.common import return_menu
from repositories.invoices import InvoiceRepository from repositories.invoices import InvoiceRepository
from config import card_info
router = Router() router = Router()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -33,9 +33,7 @@ async def activate_invoice(
invoice = await invoice_repo.get_invoice_by_id(session, invoice_id) invoice = await invoice_repo.get_invoice_by_id(session, invoice_id)
if not invoice: if not invoice:
await msg.answer( await msg.answer("⏳ Счёт истёк. Повторите попытку позже.", reply_markup=return_menu)
"⏳ Счёт истёк. Повторите попытку позже.", reply_markup=return_menu
)
return return
if invoice.status != InvoiceStatus.PENDING: if invoice.status != InvoiceStatus.PENDING:
@@ -43,9 +41,7 @@ async def activate_invoice(
return return
if not (card_info.number and card_info.holder): if not (card_info.number and card_info.holder):
await msg.answer( await msg.answer("🍃 Оплата недоступна, повторите попытку позже.", reply_markup=return_menu)
"🍃 Оплата недоступна, повторите попытку позже.", reply_markup=return_menu
)
return return
await msg.answer( await msg.answer(

View File

@@ -1,7 +1,9 @@
from aiogram import Router, F import contextlib
from aiogram.types import CallbackQuery, Message
from aiogram import F, Router
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from misc.kb import main_menu_kb from misc.kb import main_menu_kb
@@ -20,12 +22,11 @@ async def user_main_menu(
): ):
await state.clear() await state.clear()
cart_items = await order_items_repo.get_items_count_by_customer( cart_items = await order_items_repo.get_items_count_by_customer(session, msg.from_user.id)
session, msg.from_user.id
)
await msg.answer( await msg.answer(
"hii!", reply_markup=main_menu_kb(cart_items) "<b>Добрый день!</b>\n" 'Нажмите "Каталог" чтобы выбрать товар или воспользуйтесь поиском.',
) # TODO: Write a welcome message reply_markup=main_menu_kb(cart_items),
)
@router.callback_query(F.data.startswith("menu:")) @router.callback_query(F.data.startswith("menu:"))
@@ -37,14 +38,11 @@ async def user_main_menu_cb(
): ):
await state.clear() await state.clear()
try: with contextlib.suppress(Exception):
await cb.message.reply_to_message.delete() await cb.message.reply_to_message.delete()
except Exception:
...
cart_items = await order_items_repo.get_items_count_by_customer( cart_items = await order_items_repo.get_items_count_by_customer(session, cb.from_user.id)
session, cb.from_user.id
)
await cb.message.edit_text( await cb.message.edit_text(
"hii!", reply_markup=main_menu_kb(cart_items) "<b>Добрый день!</b>\n" 'Нажмите "Каталог" чтобы выбрать товар или воспользуйтесь поиском.',
) # TODO: Write a welcome message reply_markup=main_menu_kb(cart_items),
)

View File

@@ -1,10 +1,11 @@
import os import contextlib
import logging import logging
import os
from re import Match from re import Match
from aiogram import Router, F
from aiogram.types import CallbackQuery, FSInputFile from aiogram import F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, FSInputFile
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import VERIFIED_ACCOUNTS from config import VERIFIED_ACCOUNTS
@@ -23,9 +24,7 @@ logger = logging.getLogger(__name__)
@router.callback_query( @router.callback_query(
F.data.regexp(r"products:(?P<category_id>.*):(?P<page_id>\d+)").as_( F.data.regexp(r"products:(?P<category_id>.*):(?P<page_id>\d+)").as_("pagination_match")
"pagination_match"
)
) )
async def products_pagination( async def products_pagination(
cb: CallbackQuery, cb: CallbackQuery,
@@ -39,11 +38,11 @@ async def products_pagination(
pagination_data = pagination_match.groupdict() pagination_data = pagination_match.groupdict()
category_id = int(pagination_data.get("category_id")) category_id = int(pagination_data.get("category_id"))
page_id = int(pagination_data.get("page_id", 0)) page_id = int(pagination_data.get("page_id", 0))
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
view = await catalogue_service.build_category_view( view = await catalogue_service.build_category_view(
session, category_id, page=page_id session, category_id, page=page_id, include_hidden=is_admin
) )
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
kb = render_catalogue(view, is_admin=is_admin) kb = render_catalogue(view, is_admin=is_admin)
@@ -61,10 +60,15 @@ async def product_card(
await state.clear() await state.clear()
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) is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
product = await products_repo.get_product_by_id(session, product_id, include_hidden=is_admin)
if cb.from_user.id in VERIFIED_ACCOUNTS: if not product:
kb = admins.edit_product(product_id=product.id, category_id=product.category_id) await cb.answer("Товар недоступен.", show_alert=True)
return
if is_admin:
kb = admins.edit_product(product)
else: else:
order_item = await order_service.get_cart_product_amount( order_item = await order_service.get_cart_product_amount(
session, customer=cb.from_user.id, product_id=product_id session, customer=cb.from_user.id, product_id=product_id
@@ -76,9 +80,7 @@ async def product_card(
cart_amount=order_item, cart_amount=order_item,
) )
if not product.file_id and not ( if not product.file_id and not (product.img_path and os.path.isfile(product.img_path)):
product.img_path and 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=kb, reply_markup=kb,
@@ -95,10 +97,8 @@ async def product_card(
) )
return return
try: with contextlib.suppress(Exception):
await cb.message.edit_text("<b>⏳ Загружаем карточку товара...</b>") await cb.message.edit_text("<b>⏳ Загружаем карточку товара...</b>")
except TelegramBadRequest:
...
msg = await cb.message.answer_photo( msg = await cb.message.answer_photo(
FSInputFile(product.img_path), FSInputFile(product.img_path),
@@ -110,9 +110,7 @@ async def product_card(
logger.warning("didnt get a photo back wahhh :(") logger.warning("didnt get a photo back wahhh :(")
return # TODO: Add logging return # TODO: Add logging
await products_repo.add_product_file_id( await products_repo.add_product_file_id(session, product_id, file_id=msg.photo[-1].file_id)
session, product_id, file_id=msg.photo[-1].file_id
)
@router.callback_query(F.data.startswith("cart_action:")) @router.callback_query(F.data.startswith("cart_action:"))
@@ -127,7 +125,13 @@ async def cart_action(
action = cb.data.split(":")[1] action = cb.data.split(":")[1]
product_id = int(cb.data.split(":")[2]) product_id = int(cb.data.split(":")[2])
product = await products_repo.get_product_by_id(session, product_id) is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
product = await products_repo.get_product_by_id(session, product_id, include_hidden=is_admin)
if not product:
await cb.answer("Товар недоступен.", show_alert=True)
return
show_cart = True show_cart = True
if action == "add": if action == "add":

View File

@@ -1,12 +1,11 @@
from typing import Optional
import hashlib import hashlib
from aiogram import Router, F from aiogram import F, Router
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import PAGE_SIZE from config import PAGE_SIZE, VERIFIED_ACCOUNTS
from misc.kb.client import render_products from misc.kb.client import render_products
from misc.kb.common import return_menu from misc.kb.common import return_menu
from misc.redis import RedisClient from misc.redis import RedisClient
@@ -20,7 +19,7 @@ router = Router()
async def search_trigger(cb: CallbackQuery, state: FSMContext): async def search_trigger(cb: CallbackQuery, state: FSMContext):
await state.clear() await state.clear()
await cb.message.edit_text("🔍 Введите запрос:", reply_markup=return_menu) await cb.message.edit_text("<b>🔍 Введите запрос:</b>", reply_markup=return_menu)
await state.set_state(SearchStorage.query) await state.set_state(SearchStorage.query)
await state.set_data({"msg": cb.message}) await state.set_data({"msg": cb.message})
@@ -37,11 +36,13 @@ async def searching(
await state.clear() await state.clear()
await msg.delete() await msg.delete()
orig_msg: Optional[Message] = data.get("msg") or await msg.answer("⏳") orig_msg: Message | None = data.get("msg") or await msg.answer("⏳")
await orig_msg.edit_text("⏳ Поиск...") await orig_msg.edit_text("⏳ Поиск...")
is_admin = msg.from_user.id in VERIFIED_ACCOUNTS
results = await products_repo.search( results = await products_repo.search(
session, session,
msg.text, msg.text,
include_hidden=is_admin,
) )
next_cb = None next_cb = None
@@ -51,12 +52,13 @@ async def searching(
next_cb = f"search:{query_hash}:1" next_cb = f"search:{query_hash}:1"
await orig_msg.edit_text( await orig_msg.edit_text(
"here", "<b>🗒 Результаты поиска:</b>",
reply_markup=render_products( reply_markup=render_products(
results[:PAGE_SIZE], results[:PAGE_SIZE],
product_cb_factory=lambda p: f"product:{p.id}", product_cb_factory=lambda p: f"product:{p.id}",
back_cb="menu:main", back_cb="menu:main",
next_cb=next_cb, next_cb=next_cb,
is_admin=is_admin,
), ),
) )
@@ -79,7 +81,13 @@ async def search_pagination(
if not query: if not query:
await cb.message.edit_text("🍃 Попробуйте ещё раз...") await cb.message.edit_text("🍃 Попробуйте ещё раз...")
results = await products_repo.search(session, query, offset=PAGE_SIZE * page) is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
results = await products_repo.search(
session,
query,
offset=PAGE_SIZE * page,
include_hidden=is_admin,
)
await cb.message.edit_reply_markup( await cb.message.edit_reply_markup(
reply_markup=render_products( reply_markup=render_products(
@@ -88,5 +96,6 @@ async def search_pagination(
back_cb="menu:main", back_cb="menu:main",
next_cb=f"search:{q_hash}:{page + 1}" if len(results) > PAGE_SIZE else None, next_cb=f"search:{q_hash}:{page + 1}" if len(results) > PAGE_SIZE else None,
prev_cb=f"search:{q_hash}:{page - 1}" if page > 0 else None, prev_cb=f"search:{q_hash}:{page - 1}" if page > 0 else None,
is_admin=is_admin,
) )
) )

View File

@@ -1,4 +1,4 @@
from aiogram import Router, F from aiogram import F, Router
from aiogram.enums import MessageOriginType from aiogram.enums import MessageOriginType
from aiogram.types import Message from aiogram.types import Message

12
main.py
View File

@@ -1,14 +1,15 @@
import os
import asyncio import asyncio
import logging import logging
import os
from aiogram import Bot, Dispatcher from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
from redis.asyncio import Redis
from aiogram.client.session.aiohttp import AiohttpSession from aiogram.client.session.aiohttp import AiohttpSession
from redis.asyncio import Redis
from config import BOT_TOKEN, REDIS_URL from config import BOT_TOKEN, PROXY_URL, REDIS_URL
from db.base import async_session
from handlers import admins_routers, client_routers from handlers import admins_routers, client_routers
from middlewares.di import DIMiddleware from middlewares.di import DIMiddleware
from middlewares.repository import RepositoryMiddleware from middlewares.repository import RepositoryMiddleware
from middlewares.session import DBSessionMiddleware from middlewares.session import DBSessionMiddleware
@@ -19,7 +20,6 @@ from repositories.order_items import OrderItemRepository
from repositories.orders import OrderRepository from repositories.orders import OrderRepository
from repositories.products import ProductRepository from repositories.products import ProductRepository
from services import OrderService from services import OrderService
from db.base import async_session
from services.catalogue import CatalogueService from services.catalogue import CatalogueService
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
@@ -32,7 +32,7 @@ os.makedirs("static/img", exist_ok=True)
async def main(): async def main():
dp = Dispatcher() dp = Dispatcher()
aiohttp_session = AiohttpSession(proxy="http://127.0.0.1:2080") aiohttp_session = AiohttpSession(proxy=PROXY_URL)
bot = Bot( bot = Bot(
token=BOT_TOKEN, token=BOT_TOKEN,
default=DefaultBotProperties(parse_mode="HTML"), default=DefaultBotProperties(parse_mode="HTML"),

View File

@@ -2,4 +2,4 @@ from .di import DIMiddleware
from .repository import RepositoryMiddleware from .repository import RepositoryMiddleware
from .session import DBSessionMiddleware from .session import DBSessionMiddleware
__all__ = ["DIMiddleware", "RepositoryMiddleware", "DBSessionMiddleware"] __all__ = ["DBSessionMiddleware", "DIMiddleware", "RepositoryMiddleware"]

View File

@@ -1,10 +1,11 @@
from aiogram import BaseMiddleware from collections.abc import Callable
from typing import Any, Callable from typing import Any
from aiogram import BaseMiddleware
from redis.asyncio import Redis from redis.asyncio import Redis
from services.orders import OrderService
from services.catalogue import CatalogueService from services.catalogue import CatalogueService
from services.orders import OrderService
class DIMiddleware(BaseMiddleware): class DIMiddleware(BaseMiddleware):

View File

@@ -1,6 +1,6 @@
from aiogram import BaseMiddleware from aiogram import BaseMiddleware
from repositories import OrderRepository, CategoriesRepository from repositories import CategoriesRepository, OrderRepository
from repositories.invoices import InvoiceRepository from repositories.invoices import InvoiceRepository
from repositories.order_items import OrderItemRepository from repositories.order_items import OrderItemRepository
from repositories.products import ProductRepository from repositories.products import ProductRepository

View File

@@ -1,4 +0,0 @@
from misc.kb.admins import *
from misc.kb.client import *
__all__ = ["admins", "client"]

View File

@@ -1,7 +1,10 @@
from typing import Optional, Callable, Union from collections.abc import Callable
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from db.models import Product
main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder( main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
[ [
[InlineKeyboardButton(text="📦 Изменить каталог", callback_data="cat:root")], [InlineKeyboardButton(text="📦 Изменить каталог", callback_data="cat:root")],
@@ -10,22 +13,18 @@ main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
).as_markup() ).as_markup()
def back_to_product_kb( def back_to_product_kb(product_id: int | str, *, cb_factory: Callable[[int | str], str]):
product_id: Union[int, str], *, cb_factory: Callable[[int | str], str]
):
return InlineKeyboardBuilder( return InlineKeyboardBuilder(
[[InlineKeyboardButton(text="⬅️", callback_data=cb_factory(product_id))]] [[InlineKeyboardButton(text="⬅️", callback_data=cb_factory(product_id))]]
).as_markup() ).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: str | None = None
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
btns: list[list[InlineKeyboardButton]] = [] btns: list[list[InlineKeyboardButton]] = []
( (
btns.append( btns.append([InlineKeyboardButton(text="👤 Перейти", url=f"https://t.me/{mention}")])
[InlineKeyboardButton(text="👤 Перейти", url=f"https://t.me/{mention}")]
)
if has_mention if has_mention
else None else None
) )
@@ -40,9 +39,7 @@ def customer_contacts(
def payment_link(url: str) -> InlineKeyboardMarkup: def payment_link(url: str) -> InlineKeyboardMarkup:
return InlineKeyboardBuilder( return InlineKeyboardBuilder([[InlineKeyboardButton(text="💸 Оплатить", url=url)]]).as_markup()
[[InlineKeyboardButton(text="💸 Оплатить", url=url)]]
).as_markup()
def verify_payment(invoice_id: int): def verify_payment(invoice_id: int):
@@ -57,14 +54,15 @@ def verify_payment(invoice_id: int):
).as_markup() ).as_markup()
def edit_product(product_id: int, category_id: int): def edit_product(product: Product):
product_id = product.id
prefix = f"edit_product:{product_id}" prefix = f"edit_product:{product_id}"
visibility_btn = "🙈 Скрыт" if product.is_hidden else "👁️ Видим"
return InlineKeyboardBuilder( return InlineKeyboardBuilder(
[ [
[ [
InlineKeyboardButton( InlineKeyboardButton(text="✍️ Название", callback_data=f"{prefix}:name"),
text="✍️ Название", callback_data=f"{prefix}:name"
),
InlineKeyboardButton( InlineKeyboardButton(
text="✍️ Описание", text="✍️ Описание",
callback_data=f"{prefix}:description", callback_data=f"{prefix}:description",
@@ -72,9 +70,9 @@ def edit_product(product_id: int, category_id: int):
], ],
[InlineKeyboardButton(text="💸 Цена", callback_data=f"{prefix}:price")], [InlineKeyboardButton(text="💸 Цена", callback_data=f"{prefix}:price")],
[ [
InlineKeyboardButton(text="👁️", callback_data=f"{prefix}:hide"), InlineKeyboardButton(text=visibility_btn, callback_data=f"{prefix}:hide"),
InlineKeyboardButton(text="❌", callback_data=f"{prefix}:delete"), InlineKeyboardButton(text="❌", callback_data=f"{prefix}:delete"),
], ],
[InlineKeyboardButton(text="⬅️", callback_data=f"cat:{category_id}")], [InlineKeyboardButton(text="⬅️", callback_data=f"cat:{product.category_id}")],
] ]
).as_markup() ).as_markup()

View File

@@ -1,4 +1,4 @@
from typing import Optional, Callable, Union from collections.abc import Callable
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -6,15 +6,15 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import PAGE_SIZE from config import PAGE_SIZE
from db.models import Category, Product from db.models import Category, Product
from dto.cart import CartItemDTO from dto.cart import CartItemDTO
from dto.catalogue import CatalogueView, CatalogueType from dto.catalogue import CatalogueType, CatalogueView
from misc.mapper import serialize_cat_id from misc.mapper import serialize_cat_id
def pagination_row( def pagination_row(
*, *,
page: int, page: int,
prev_cb: Optional[str] = None, prev_cb: str | None = None,
next_cb: Optional[str] = None, next_cb: str | None = None,
) -> list[InlineKeyboardButton]: ) -> list[InlineKeyboardButton]:
row = [] row = []
@@ -39,22 +39,18 @@ def cart_btn(cart_items: int) -> InlineKeyboardButton:
def get_back_to_catalogue( def get_back_to_catalogue(
parent_id: int, parent_id: int,
*, *,
text: Optional[str] = "⬅️ Назад", text: str | None = "⬅️ Назад",
show_controls: bool = False, show_controls: bool = False,
cat_id: Optional[Union[str, int]] = None, cat_id: str | int | None = None,
fallback_cb: Optional[str] = "menu:main", fallback_cb: str | None = "menu:main",
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
if show_controls and cat_id: if show_controls and cat_id:
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(text="➕ Категория", callback_data=f"new:category:{cat_id}"),
text="➕ Категория", callback_data=f"new:category:{cat_id}" InlineKeyboardButton(text="➕ Товар", callback_data=f"new:product:{cat_id}"),
),
InlineKeyboardButton(
text="➕ Товар", callback_data=f"new:product:{cat_id}"
),
) )
builder.row( builder.row(
@@ -82,7 +78,7 @@ def main_menu_kb(cart_items: int) -> InlineKeyboardMarkup:
[ [
[ [
InlineKeyboardButton(text="📦 Каталог", callback_data="cat:root"), InlineKeyboardButton(text="📦 Каталог", callback_data="cat:root"),
InlineKeyboardButton(text="🔍", callback_data="find"), InlineKeyboardButton(text="🔍 Поиск", callback_data="find"),
], ],
[cart_btn(cart_items)], [cart_btn(cart_items)],
] ]
@@ -91,10 +87,10 @@ def main_menu_kb(cart_items: int) -> InlineKeyboardMarkup:
def render_category( def render_category(
children: list[Category], children: list[Category],
parent_id: Optional[Union[str, int]] = None, parent_id: str | int | None = None,
show_menu: bool = False, show_menu: bool = False,
*, *,
cat_id: Optional[Union[str, int]] = None, cat_id: str | int | None = None,
category_cb_factory: Callable[[Category], str], category_cb_factory: Callable[[Category], str],
is_admin: bool = False, is_admin: bool = False,
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
@@ -116,9 +112,7 @@ def render_category(
text="➕ Категория", text="➕ Категория",
callback_data=f"new:category:{cat_id}", callback_data=f"new:category:{cat_id}",
), ),
InlineKeyboardButton( InlineKeyboardButton(text="✍️", callback_data=f"edit:category:{cat_id}"),
text="✍️", callback_data=f"edit:category:{cat_id}"
),
] ]
) )
@@ -126,7 +120,10 @@ def render_category(
nav.append([InlineKeyboardButton(text="⬅️ Назад", callback_data="menu:main")]) nav.append([InlineKeyboardButton(text="⬅️ Назад", callback_data="menu:main")])
elif parent_id is not None: elif parent_id is not None:
nav.append( nav.append(
[InlineKeyboardButton(text="⬅️ Назад", callback_data=f"cat:{parent_id}")] [
InlineKeyboardButton(text="⬅️ Назад", callback_data=f"cat:{parent_id}"),
InlineKeyboardButton(text="🏠", callback_data="menu:main"),
]
) )
return InlineKeyboardBuilder( return InlineKeyboardBuilder(
@@ -138,22 +135,25 @@ def render_products(
products: list[Product], products: list[Product],
*, *,
product_cb_factory: Callable[[Product], str], product_cb_factory: Callable[[Product], str],
back_cb: Optional[str] = None, back_cb: str | None = None,
prev_cb: Optional[str] = None, prev_cb: str | None = None,
next_cb: Optional[str] = None, next_cb: str | None = None,
columns: int = 2, columns: int = 2,
is_admin: bool = False, is_admin: bool = False,
cat_id: Optional[int] = None, cat_id: int | None = None,
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
if back_cb: if back_cb:
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=back_cb)) builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data=back_cb),
InlineKeyboardButton(text="🏠", callback_data="menu:main"),
)
for i in range(0, len(products), columns): for i in range(0, len(products), columns):
row = [ row = [
InlineKeyboardButton( InlineKeyboardButton(
text=p.name, text=f"{p.name} {'🙈' if is_admin and p.is_hidden else ''}".strip(),
callback_data=product_cb_factory(p), callback_data=product_cb_factory(p),
) )
for p in products[i : i + columns] for p in products[i : i + columns]
@@ -161,9 +161,7 @@ def render_products(
builder.row(*row) builder.row(*row)
if is_admin: if is_admin:
builder.row( builder.row(InlineKeyboardButton(text="➕ Товар", callback_data=f"new:product:{cat_id}"))
InlineKeyboardButton(text="➕ Товар", callback_data=f"new:product:{cat_id}")
)
nav_buttons = [] nav_buttons = []
if prev_cb: if prev_cb:
@@ -193,12 +191,8 @@ def render_catalogue(view: CatalogueView, *, is_admin: bool):
products=view.products[:PAGE_SIZE], # type: ignore products=view.products[:PAGE_SIZE], # type: ignore
product_cb_factory=lambda p: f"product:{p.id}", product_cb_factory=lambda p: f"product:{p.id}",
back_cb=f"cat:{view.parent_id}", back_cb=f"cat:{view.parent_id}",
prev_cb=( prev_cb=(f"products:{view.category}:{view.page - 1}" if view.page > 0 else None),
f"products:{view.category}:{view.page - 1}" if view.page > 0 else None next_cb=(f"products:{view.category}:{view.page + 1}" if view.has_next else None),
),
next_cb=(
f"products:{view.category}:{view.page + 1}" if view.has_next else None
),
is_admin=is_admin, is_admin=is_admin,
cat_id=view.category, cat_id=view.category,
) )
@@ -216,13 +210,9 @@ def render_product_interactions(
if show_cart: if show_cart:
btns.append( btns.append(
[ [
InlineKeyboardButton( InlineKeyboardButton(text="➖", callback_data=f"cart_action:remove:{product_id}"),
text="➖", callback_data=f"cart_action:remove:{product_id}"
),
InlineKeyboardButton(text=f"🛒 {cart_amount}", callback_data="..."), InlineKeyboardButton(text=f"🛒 {cart_amount}", callback_data="..."),
InlineKeyboardButton( InlineKeyboardButton(text="➕", callback_data=f"cart_action:add:{product_id}"),
text="➕", callback_data=f"cart_action:add:{product_id}"
),
] ]
) )
btns.append( btns.append(
@@ -243,7 +233,12 @@ def render_product_interactions(
] ]
) )
btns.append([InlineKeyboardButton(text="⬅️", callback_data=f"cat:{category_id}")]) btns.append(
[
InlineKeyboardButton(text="⬅️", callback_data=f"cat:{category_id}"),
InlineKeyboardButton(text="🏠", callback_data="menu:main"),
]
)
return InlineKeyboardBuilder(btns).as_markup() return InlineKeyboardBuilder(btns).as_markup()
@@ -252,9 +247,9 @@ def render_cart(
cart_items: list[CartItemDTO], cart_items: list[CartItemDTO],
*, *,
page: int = 0, page: int = 0,
show_prev: Optional[bool] = False, show_prev: bool | None = False,
show_next: Optional[bool] = False, show_next: bool | None = False,
show_purchase: Optional[bool] = False, show_purchase: bool | None = False,
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
btns = [ btns = [
InlineKeyboardButton( InlineKeyboardButton(
@@ -284,9 +279,7 @@ def render_cart(
) )
if show_purchase: if show_purchase:
markup.append( markup.append([InlineKeyboardButton(text="🔷 Оформить заказ", callback_data="checkout")])
[InlineKeyboardButton(text="🔷 Оформить заказ", callback_data="checkout")]
)
return InlineKeyboardBuilder(markup).as_markup() return InlineKeyboardBuilder(markup).as_markup()

View File

@@ -1,5 +1,4 @@
import logging import logging
from typing import Optional
from redis.asyncio import Redis from redis.asyncio import Redis
@@ -18,7 +17,7 @@ class RedisClient:
logger.exception(e) logger.exception(e)
return False return False
async def get_search_query(self, query_hash: str) -> Optional[str]: async def get_search_query(self, query_hash: str) -> str | None:
try: try:
res = await self.r.get(f"search:{query_hash}") res = await self.r.get(f"search:{query_hash}")
return res.decode() return res.decode()

View File

@@ -1,4 +1,4 @@
from aiogram.fsm.state import StatesGroup, State from aiogram.fsm.state import State, StatesGroup
class CheckoutStorage(StatesGroup): class CheckoutStorage(StatesGroup):

View File

@@ -1,6 +1,7 @@
import base64 import base64
import json import json
from typing import Any from typing import Any
import phonenumbers import phonenumbers
from phonenumbers import NumberParseException from phonenumbers import NumberParseException

View File

@@ -1,11 +1,56 @@
[tool.black] [tool.black]
line-length = 88 line-length = 100
target-version = ["py314"] target-version = ['py313']
include = '\.pyi?$'
extend-exclude = '''
/(
\.git
| venv
| build
| dist
| alembic
)/
'''
skip-string-normalization = false [tool.ruff]
skip-magic-trailing-comma = false line-length = 100
target-version = "py313"
preview = false exclude = [
".git",
"venv",
"build",
"dist",
"alembic",
]
[tool.ruff.lint] [tool.ruff.lint]
ignore=["F405", "F403"] select = [
"E",
"W",
"F",
"I",
"N",
"UP",
"B",
"SIM",
"PL",
"RUF",
"TID",
"PT",
]
ignore = [
"E501",
"D100",
"D104",
"G004",
"PLR0913",
"RUF001",
"RUF002",
"RUF003",
"B008"
]
[tool.ruff.lint.isort]
combine-as-imports = true

View File

@@ -1,11 +1,11 @@
from .orders import OrderRepository
from .categories import CategoriesRepository from .categories import CategoriesRepository
from .products import ProductRepository
from .invoices import InvoiceRepository from .invoices import InvoiceRepository
from .orders import OrderRepository
from .products import ProductRepository
__all__ = [ __all__ = [
"OrderRepository",
"CategoriesRepository", "CategoriesRepository",
"ProductRepository",
"InvoiceRepository", "InvoiceRepository",
"OrderRepository",
"ProductRepository",
] ]

View File

@@ -1,20 +1,17 @@
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from db.models import Category from db.models import Category
class CategoriesRepository: class CategoriesRepository:
async def get_category_by_id( async def get_category_by_id(self, session: AsyncSession, category_id: int) -> Category | None:
self, session: AsyncSession, category_id: int
) -> Optional[Category]:
stmt = select(Category).where(Category.id == category_id) stmt = select(Category).where(Category.id == category_id)
return await session.scalar(stmt) return await session.scalar(stmt)
async def get_categories_by_parent_id( async def get_categories_by_parent_id(
self, session: AsyncSession, parent_id: Optional[int] = None self, session: AsyncSession, parent_id: int | None = None
) -> list[Category]: ) -> list[Category]:
stmt = select(Category).where(Category.parent_id == parent_id) stmt = select(Category).where(Category.parent_id == parent_id)
result = await session.scalars(stmt) result = await session.scalars(stmt)
@@ -31,9 +28,7 @@ class CategoriesRepository:
parent = aliased(Category) parent = aliased(Category)
cte = cte.union_all( cte = cte.union_all(
select(parent.id, parent.parent_id, parent.name).join( select(parent.id, parent.parent_id, parent.name).join(cte, cte.c.parent_id == parent.id)
cte, cte.c.parent_id == parent.id
)
) )
stmt = select(cte) stmt = select(cte)
@@ -45,7 +40,7 @@ class CategoriesRepository:
return list(reversed(rows)) return list(reversed(rows))
async def add_category( async def add_category(
self, session: AsyncSession, *, name: str, parent_id: Optional[int] self, session: AsyncSession, *, name: str, parent_id: int | None
) -> Category: ) -> Category:
category = Category(name=name, parent_id=parent_id) category = Category(name=name, parent_id=parent_id)
@@ -54,9 +49,7 @@ class CategoriesRepository:
return category return category
async def update_category_name( async def update_category_name(self, session: AsyncSession, category_id: int, value: str):
self, session: AsyncSession, category_id: int, value: str
):
category = await self.get_category_by_id(session, category_id) category = await self.get_category_by_id(session, category_id)
category.name = value category.name = value

View File

@@ -1,9 +1,7 @@
from typing import Optional
from sqlalchemy import select from sqlalchemy import select
from db.models import Invoice
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from db.models import Invoice
from db.models.invoices import InvoiceStatus from db.models.invoices import InvoiceStatus
@@ -14,7 +12,7 @@ class InvoiceRepository:
*, *,
amount: int, amount: int,
creator_id: int, creator_id: int,
inline_message_id: Optional[int] = None, inline_message_id: int | None = None,
status: InvoiceStatus = InvoiceStatus.PENDING, status: InvoiceStatus = InvoiceStatus.PENDING,
) -> Invoice: ) -> Invoice:
invoice = Invoice( invoice = Invoice(
@@ -28,9 +26,7 @@ class InvoiceRepository:
await session.commit() await session.commit()
return invoice return invoice
async def get_invoice_by_id( async def get_invoice_by_id(self, session: AsyncSession, invoice_id: int) -> Invoice | None:
self, session: AsyncSession, invoice_id: int
) -> Optional[Invoice]:
stmt = select(Invoice).where(Invoice.id == invoice_id) stmt = select(Invoice).where(Invoice.id == invoice_id)
return await session.scalar(stmt) return await session.scalar(stmt)

View File

@@ -1,26 +1,18 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import delete, func, select from sqlalchemy import delete, func, select
from typing import Optional from sqlalchemy.ext.asyncio import AsyncSession
from db.models.orders import Order, OrderItem, OrderStatus from db.models.orders import Order, OrderItem, OrderStatus
class OrderItemRepository: class OrderItemRepository:
async def get_item_by_id( async def get_item_by_id(self, session: AsyncSession, item_id: int) -> OrderItem | None:
self, session: AsyncSession, item_id: int
) -> Optional[OrderItem]:
stmt = select(OrderItem).where(OrderItem.id == item_id) stmt = select(OrderItem).where(OrderItem.id == item_id)
return await session.scalar(stmt) return await session.scalar(stmt)
async def get_items_by_order( async def get_items_by_order(
self, session: AsyncSession, *, order_id: int, limit: int, offset: int = 0 self, session: AsyncSession, *, order_id: int, limit: int, offset: int = 0
) -> list[OrderItem]: ) -> list[OrderItem]:
stmt = ( stmt = select(OrderItem).where(OrderItem.order_id == order_id).limit(limit).offset(offset)
select(OrderItem)
.where(OrderItem.order_id == order_id)
.limit(limit)
.offset(offset)
)
result = await session.scalars(stmt) result = await session.scalars(stmt)
return list(result) return list(result)
@@ -29,9 +21,7 @@ class OrderItemRepository:
stmt = select(func.count(OrderItem.id)).where(OrderItem.order_id == order_id) stmt = select(func.count(OrderItem.id)).where(OrderItem.order_id == order_id)
return await session.scalar(stmt) or 0 return await session.scalar(stmt) or 0
async def get_items_count_by_customer( async def get_items_count_by_customer(self, session: AsyncSession, customer: int) -> int:
self, session: AsyncSession, customer: int
) -> int:
count = await session.scalar( count = await session.scalar(
select(func.count(OrderItem.id)) select(func.count(OrderItem.id))
.join(Order) .join(Order)
@@ -44,7 +34,7 @@ class OrderItemRepository:
async def get_item_by_order_and_product( async def get_item_by_order_and_product(
self, session: AsyncSession, *, order_id: int, product_id: int self, session: AsyncSession, *, order_id: int, product_id: int
) -> Optional[OrderItem]: ) -> OrderItem | None:
stmt = ( stmt = (
select(OrderItem) select(OrderItem)
.where(OrderItem.order_id == order_id) .where(OrderItem.order_id == order_id)
@@ -68,9 +58,7 @@ class OrderItemRepository:
product_id: int, product_id: int,
quantity: int = 1, quantity: int = 1,
) -> OrderItem: ) -> OrderItem:
order_item = OrderItem( order_item = OrderItem(order_id=order_id, product_id=product_id, quantity=quantity)
order_id=order_id, product_id=product_id, quantity=quantity
)
session.add(order_item) session.add(order_item)
await session.commit() await session.commit()

View File

@@ -1,30 +1,22 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import delete, select from sqlalchemy import delete, select
from typing import Optional from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from db.models import Order, OrderStatus, OrderItem from db.models import Order, OrderItem, OrderStatus
class OrderRepository: class OrderRepository:
async def get_order_by_id( async def get_order_by_id(self, session: AsyncSession, order_id: int) -> Order | None:
self, session: AsyncSession, order_id: int
) -> Optional[Order]:
stmt = select(Order).where(Order.id == order_id) stmt = select(Order).where(Order.id == order_id)
return await session.scalar(stmt) return await session.scalar(stmt)
async def get_orders_by_user( async def get_orders_by_user(self, session: AsyncSession, customer: int) -> list[Order]:
self, session: AsyncSession, customer: int
) -> list[Order]:
stmt = select(Order).where(Order.customer == customer) stmt = select(Order).where(Order.customer == customer)
result = await session.scalars(stmt) result = await session.scalars(stmt)
return list(result) return list(result)
async def get_draft_order_by_user( async def get_draft_order_by_user(self, session: AsyncSession, customer: int) -> Order | None:
self, session: AsyncSession, customer: int
) -> Optional[Order]:
stmt = ( stmt = (
select(Order) select(Order)
.where(Order.customer == customer) .where(Order.customer == customer)
@@ -59,9 +51,7 @@ class OrderRepository:
await session.execute(stmt) await session.execute(stmt)
await session.commit() await session.commit()
async def update_order_status( async def update_order_status(self, session: AsyncSession, order_id: int, status: OrderStatus):
self, session: AsyncSession, order_id: int, status: OrderStatus
):
order = await self.get_order_by_id(session, order_id) order = await self.get_order_by_id(session, order_id)
order.status = status order.status = status

View File

@@ -1,7 +1,6 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from typing import Optional
from config import PAGE_SIZE from config import PAGE_SIZE
from db.models import Product from db.models import Product
@@ -9,21 +8,28 @@ from db.models import Product
class ProductRepository: class ProductRepository:
async def get_product_by_id( async def get_product_by_id(
self, session: AsyncSession, product_id: int self, session: AsyncSession, product_id: int, *, include_hidden: bool = True
) -> Optional[Product]: ) -> Product | None:
stmt = select(Product).where(Product.id == product_id) stmt = select(Product).where(Product.id == product_id)
if not include_hidden:
stmt = stmt.where(Product.is_hidden.is_(False))
return await session.scalar(stmt) return await session.scalar(stmt)
async def get_product_by_category( async def get_product_by_category(
self, session: AsyncSession, category_id: int, *, limit: int, offset: int self,
session: AsyncSession,
category_id: int,
*,
limit: int,
offset: int,
include_hidden: bool = False,
) -> list[Product]: ) -> list[Product]:
stmt = ( stmt = select(Product).where(Product.category_id == category_id)
select(Product)
.where(Product.category_id == category_id) if not include_hidden:
.order_by(Product.id) stmt = stmt.where(Product.is_hidden.is_(False))
.offset(offset)
.limit(limit) stmt = stmt.order_by(Product.id).offset(offset).limit(limit)
)
result = await session.scalars(stmt) result = await session.scalars(stmt)
@@ -44,13 +50,17 @@ class ProductRepository:
*, *,
limit: int = PAGE_SIZE + 1, limit: int = PAGE_SIZE + 1,
offset: int = 0, offset: int = 0,
) -> list[Optional[Product]]: include_hidden: bool = False,
) -> list[Product | None]:
ts_query = func.plainto_tsquery("simple", query) ts_query = func.plainto_tsquery("simple", query)
stmt = select(Product).where(Product.search_vector.op("@@")(ts_query))
if not include_hidden:
stmt = stmt.where(Product.is_hidden.is_(False))
stmt = ( stmt = (
select(Product) stmt.order_by(func.ts_rank(Product.search_vector, ts_query).desc())
.where(Product.search_vector.op("@@")(ts_query))
.order_by(func.ts_rank(Product.search_vector, ts_query).desc())
.limit(limit) .limit(limit)
.offset(offset) .offset(offset)
) )
@@ -59,9 +69,7 @@ class ProductRepository:
return list(res) return list(res)
async def update_product_name_by_id( async def update_product_name_by_id(self, session: AsyncSession, product_id: int, name: str):
self, session: AsyncSession, product_id: int, name: str
):
product = await self.get_product_by_id(session, product_id=product_id) product = await self.get_product_by_id(session, product_id=product_id)
product.name = name product.name = name
@@ -77,11 +85,21 @@ class ProductRepository:
await session.commit() await session.commit()
return product return product
async def update_product_price_by_id( async def update_product_price_by_id(self, session: AsyncSession, product_id: int, price: str):
self, session: AsyncSession, product_id: int, price: str
):
product = await self.get_product_by_id(session, product_id=product_id) product = await self.get_product_by_id(session, product_id=product_id)
product.price = price product.price = price
await session.commit() await session.commit()
return product return product
async def toggle_product_hidden_by_id(
self, session: AsyncSession, product_id: int
) -> Product | None:
product = await self.get_product_by_id(session, product_id=product_id)
if not product:
return None
product.is_hidden = not product.is_hidden
await session.commit()
return product

View File

@@ -4,7 +4,6 @@ python-dotenv
black black
asyncpg asyncpg
sqlalchemy[asyncio] sqlalchemy[asyncio]
psycopg2
phonenumbers phonenumbers
ruff ruff
redis redis

View File

@@ -1,11 +1,10 @@
from typing import Union, Optional
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import PAGE_SIZE
from dto.catalogue import CatalogueType, CatalogueView from dto.catalogue import CatalogueType, CatalogueView
from misc.mapper import parse_cat_id, serialize_cat_id from misc.mapper import parse_cat_id, serialize_cat_id
from misc.texts import get_breadcrumps_path from misc.texts import get_breadcrumps_path
from repositories import ProductRepository, CategoriesRepository from repositories import CategoriesRepository, ProductRepository
from config import PAGE_SIZE
class CatalogueService: class CatalogueService:
@@ -16,14 +15,17 @@ class CatalogueService:
self.categories_repo = categories_repo self.categories_repo = categories_repo
async def build_category_view( async def build_category_view(
self, session: AsyncSession, cat_id: Optional[Union[int, str]], page: int = 0 self,
session: AsyncSession,
cat_id: int | str | None,
page: int = 0,
*,
include_hidden: bool = False,
) -> CatalogueView: ) -> CatalogueView:
cat_id = parse_cat_id(cat_id) cat_id = parse_cat_id(cat_id)
view = CatalogueView(page=page) view = CatalogueView(page=page)
children = await self.categories_repo.get_categories_by_parent_id( children = await self.categories_repo.get_categories_by_parent_id(session, cat_id)
session, cat_id
)
category = await self.categories_repo.get_category_by_id(session, cat_id) category = await self.categories_repo.get_category_by_id(session, cat_id)
if not category: # Root category if not category: # Root category
@@ -35,7 +37,11 @@ class CatalogueService:
if not children: # Products if not children: # Products
products = await self.product_repo.get_product_by_category( products = await self.product_repo.get_product_by_category(
session, cat_id, limit=PAGE_SIZE + 1, offset=view.page * PAGE_SIZE session,
cat_id,
limit=PAGE_SIZE + 1,
offset=view.page * PAGE_SIZE,
include_hidden=include_hidden,
) )
if products: if products:
view.view_type = CatalogueType.PRODUCTS view.view_type = CatalogueType.PRODUCTS
@@ -51,7 +57,7 @@ class CatalogueService:
return view return view
async def get_path(self, session: AsyncSession, cat_id: Union[int, str]): async def get_path(self, session: AsyncSession, cat_id: int | str):
category = int(cat_id) if isinstance(cat_id, int) or cat_id.isdigit() else None category = int(cat_id) if isinstance(cat_id, int) or cat_id.isdigit() else None
products = await self.categories_repo.get_category_path(session, category) products = await self.categories_repo.get_category_path(session, category)

View File

@@ -1,5 +1,7 @@
import logging import logging
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import PAGE_SIZE from config import PAGE_SIZE
from db.models.orders import OrderStatus from db.models.orders import OrderStatus
from dto.cart import CartDTO, CartItemDTO from dto.cart import CartDTO, CartItemDTO
@@ -9,13 +11,11 @@ from repositories.orders import OrderRepository
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class OrderServiceException(Exception): ... class OrderServiceError(Exception): ...
class OrderService: class OrderService:
def __init__( def __init__(self, order_repo: OrderRepository, order_items_repo: OrderItemRepository):
self, order_repo: OrderRepository, order_items_repo: OrderItemRepository
):
self.order_repo = order_repo self.order_repo = order_repo
self.order_items_repo = order_items_repo self.order_items_repo = order_items_repo
@@ -34,9 +34,7 @@ class OrderService:
order = await self.order_repo.get_draft_order_by_user(session, customer) order = await self.order_repo.get_draft_order_by_user(session, customer)
if not order: if not order:
order = await self.order_repo.create_order( order = await self.order_repo.create_order(session, customer, OrderStatus.DRAFT)
session, customer, OrderStatus.DRAFT
)
order_item = await self.order_items_repo.get_item_by_order_and_product( order_item = await self.order_items_repo.get_item_by_order_and_product(
session, order_id=order.id, product_id=product_id session, order_id=order.id, product_id=product_id
@@ -65,7 +63,7 @@ class OrderService:
order = await self.order_repo.get_draft_order_by_user(session, customer) order = await self.order_repo.get_draft_order_by_user(session, customer)
if not order: if not order:
raise OrderServiceException( raise OrderServiceError(
"attempt of removing product from non-existing cart, potential callback_query exploit, ignoring..." "attempt of removing product from non-existing cart, potential callback_query exploit, ignoring..."
) )
@@ -74,7 +72,7 @@ class OrderService:
) )
if not order_item: if not order_item:
raise OrderServiceException( raise OrderServiceError(
"attempt of decreasing quantity of non-existing order_item, potential callback_query exploit, ignoring..." "attempt of decreasing quantity of non-existing order_item, potential callback_query exploit, ignoring..."
) )
@@ -88,13 +86,11 @@ class OrderService:
return new_quantity return new_quantity
async def clear_from_cart( async def clear_from_cart(self, session: AsyncSession, *, customer: int, product_id: int):
self, session: AsyncSession, *, customer: int, product_id: int
):
order = await self.order_repo.get_draft_order_by_user(session, customer) order = await self.order_repo.get_draft_order_by_user(session, customer)
if not order: if not order:
raise OrderServiceException( raise OrderServiceError(
"attempt of removing product from non-existing cart, potential callback_query exploit, ignoring..." "attempt of removing product from non-existing cart, potential callback_query exploit, ignoring..."
) )
@@ -103,7 +99,7 @@ class OrderService:
) )
if not order_item: if not order_item:
raise OrderServiceException( raise OrderServiceError(
"attempt of decreasing quantity of non-existing order_item, potential callback_query exploit, ignoring..." "attempt of decreasing quantity of non-existing order_item, potential callback_query exploit, ignoring..."
) )
@@ -162,9 +158,7 @@ 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( async def build_full_cart_dto(self, session: AsyncSession, customer: int) -> CartDTO:
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 = []