invoices, order handling, db updates

This commit is contained in:
2026-02-14 23:09:21 +07:00
parent a3aaafd7ac
commit 50b1a8f80c
28 changed files with 508 additions and 44 deletions

View File

@@ -0,0 +1,44 @@
"""invoice id -> text fs
Revision ID: 536f700c6d2b
Revises: f7372a4db67d
Create Date: 2026-02-14 22:54:39.201321
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "536f700c6d2b"
down_revision: Union[str, Sequence[str], None] = "f7372a4db67d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"invoices",
"inline_message_id",
existing_type=sa.INTEGER(),
type_=sa.String(),
existing_nullable=True,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"invoices",
"inline_message_id",
existing_type=sa.String(),
type_=sa.INTEGER(),
existing_nullable=True,
)
# ### end Alembic commands ###

View File

@@ -0,0 +1,47 @@
"""invoices creation
Revision ID: ac19c319304a
Revises: 2c9553f99e03
Create Date: 2026-02-14 22:20:47.308942
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "ac19c319304a"
down_revision: Union[str, Sequence[str], None] = "2c9553f99e03"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"invoices",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("creator_id", sa.BIGINT(), nullable=False),
sa.Column("amount", sa.BIGINT(), nullable=False),
sa.Column(
"status",
sa.Enum("PENDING", "PAID", "EXPIRED", name="invoicestatus"),
nullable=False,
),
sa.Column("inline_message_id", sa.BIGINT(), nullable=True),
sa.Column(
"created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("invoices")
# ### end Alembic commands ###

View File

@@ -0,0 +1,44 @@
"""invoice id -> text
Revision ID: f7372a4db67d
Revises: ac19c319304a
Create Date: 2026-02-14 22:54:03.047715
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "f7372a4db67d"
down_revision: Union[str, Sequence[str], None] = "ac19c319304a"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"invoices",
"inline_message_id",
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"invoices",
"inline_message_id",
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True,
)
# ### end Alembic commands ###

View File

@@ -14,7 +14,11 @@ POSTGRES_URL = os.getenv(
"postgresql+asyncpg://postgres:HEXDEVFUCKINGSUCKS!@localhost:5432/postgres", "postgresql+asyncpg://postgres:HEXDEVFUCKINGSUCKS!@localhost:5432/postgres",
) )
POST_TOKEN = os.getenv("POST_TOKEN", "")
POST_AUTH_KEY = os.getenv("POST_AUTH_KEY", "")
### Constants ### ### Constants ###
PAGE_SIZE = 4 PAGE_SIZE = 4
VERIFIED_ACCOUNTS = [1026030711] VERIFIED_ACCOUNTS = [1026030711, 8480400744]
NOTIFICATION_CHANNEL = -1003849564110

View File

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

24
db/models/invoices.py Normal file
View File

@@ -0,0 +1,24 @@
from datetime import datetime
from enum import Enum as E_cls
from sqlalchemy import DateTime, func, Enum
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import BIGINT
from db.base import Base
class InvoiceStatus(E_cls):
PENDING = "PENDING"
PAID = "PAID"
EXPIRED = "EXPIRED"
class Invoice(Base):
__tablename__ = "invoices"
id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True)
creator_id: Mapped[int] = mapped_column(BIGINT)
amount: Mapped[int] = mapped_column(BIGINT)
status: Mapped[InvoiceStatus] = mapped_column(Enum(InvoiceStatus))
inline_message_id: Mapped[str] = mapped_column(nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())

View File

@@ -11,8 +11,3 @@ class CheckoutContext:
name: Optional[str] = None name: Optional[str] = None
phone: Optional[str] = None phone: Optional[str] = None
address: Optional[str] = None address: Optional[str] = None
@dataclass
class PaymentContext:
notification_msg: Message

View File

@@ -2,10 +2,21 @@
set -e set -e
echo "checking venv..."
if [[ "$VIRTUAL_ENV" != "" ]]
then
echo "process is in venv, skipping."
else
echo "entering venv."
source $(pwd)/venv/bin/activate
fi
echo
echo "formatting..." echo "formatting..."
ruff check . --fix ruff check . --fix
black . black .
echo
echo "checking psql..." echo "checking psql..."
if ! pg_isready -h localhost -p 5432; then if ! pg_isready -h localhost -p 5432; then
echo "psql is down, try again" >&2 echo "psql is down, try again" >&2
@@ -13,8 +24,10 @@ if ! pg_isready -h localhost -p 5432; then
fi fi
echo "psql is up and is recieving connections." echo "psql is up and is recieving connections."
echo
echo "running migrations..." echo "running migrations..."
alembic upgrade head alembic upgrade head
echo
echo "starting the bot..." echo "starting the bot..."
exec python main.py exec python main.py

View File

@@ -1 +1,3 @@
routers = [] from .inline_mode import router as inline_router
routers = [inline_router]

View File

@@ -0,0 +1,71 @@
import logging
from aiogram import Router, F
from aiogram.types import (
ChosenInlineResult,
InlineQuery,
InlineQueryResultArticle,
InputTextMessageContent,
)
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.utils import b64_to_dict, dict_to_b64
from repositories.invoices import InvoiceRepository
router = Router()
logger = logging.getLogger(__name__)
@router.inline_query(IsVerified(), F.query.cast(int).as_("amount"))
async def send_invoice(iq: InlineQuery, amount: int):
results = [
InlineQueryResultArticle(
id=dict_to_b64({"a": amount}),
title=f"💸 Счёт на {amount}",
input_message_content=InputTextMessageContent(
message_text="⏳ Создаю счёт..."
),
reply_markup=placeholder_kb,
description="Нажмите, чтобы создать счёт.",
),
]
await iq.answer(results, is_personal=True)
@router.chosen_inline_result()
async def chosen_inline(
ev: ChosenInlineResult, session: AsyncSession, invoice_repo: InvoiceRepository
):
data = b64_to_dict(ev.result_id)
amount = data.get("a")
if not amount:
await ev.bot.edit_message_text(
inline_message_id=ev.inline_message_id, text="🍃 Что-то пошло не так..."
)
logger.critical("amount is null during the invoice creation.")
return
try:
amount = int(amount)
except Exception as e:
await ev.bot.edit_message_text(
inline_message_id=ev.inline_message_id, text="🍃 Что-то пошло не так..."
)
logger.exception(e)
return
invoice = await invoice_repo.create_invoice(
session,
amount=amount,
creator_id=ev.from_user.id,
inline_message_id=ev.inline_message_id,
)
invoice_link = await create_start_link(ev.bot, f"invoice:{invoice.id}", encode=True)
await ev.bot.edit_message_text(
inline_message_id=ev.inline_message_id,
text=f"<b>💸 Счёт на {amount}₽.</b>",
reply_markup=payment_link(invoice_link),
)

View File

@@ -1,3 +1,4 @@
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 .catalogue import router as catalogue_router
from .products import router as products_router from .products import router as products_router
@@ -6,6 +7,7 @@ from .checkout import router as checkout_router
from .security import router as security_router from .security import router as security_router
routers = [ routers = [
invoice_router,
menu_router, menu_router,
catalogue_router, catalogue_router,
products_router, products_router,

View File

@@ -1,9 +1,13 @@
import logging
from typing import Optional
from aiogram import Router, F from aiogram import Router, F
from aiogram.types import CallbackQuery, Message from aiogram.types import CallbackQuery, Message
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import NOTIFICATION_CHANNEL
from dto.checkout import CheckoutContext from dto.checkout import CheckoutContext
from misc.kb.admins import customer_contacts
from misc.kb.client import ( from misc.kb.client import (
order_confirmation, order_confirmation,
order_specs_confirmation, order_specs_confirmation,
@@ -16,6 +20,7 @@ from repositories.orders import OrderRepository
from services.orders import OrderService from services.orders import OrderService
router = Router() router = Router()
logger = logging.getLogger(__name__)
@router.callback_query(F.data == "checkout") @router.callback_query(F.data == "checkout")
@@ -119,4 +124,55 @@ async def checkout_address(msg: Message, state: FSMContext):
f"<i>📍 Ваш адрес: {ctx.address}</i>\n\n" f"<i>📍 Ваш адрес: {ctx.address}</i>\n\n"
"━━━━━━━━━━━━━━", "━━━━━━━━━━━━━━",
reply_markup=order_specs_confirmation(ctx.order_id), reply_markup=order_specs_confirmation(ctx.order_id),
) # TODO: State-driven Payment System. )
await state.set_state(CheckoutStorage.confirmation)
await state.set_data({"ctx": ctx})
@router.callback_query(CheckoutStorage.confirmation)
@router.callback_query(F.data.startswith("payment:"))
async def payment_create(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
order_service: OrderService,
):
data = await state.get_data()
ctx: Optional[CheckoutContext] = data.get("ctx")
if not ctx:
await cb.message.edit_text(
"🍃 Что-то пошло не так, повторите попытку позже...",
reply_markup=return_menu,
)
logger.warning("ctx not found on confirmation screen")
return
await state.clear()
await cb.message.edit_text(
"<b>🟢 Ваш заказ успешно отправлен!</b>\n"
"<i>📦 Мы свяжемся с вами для уточнения деталей в ближайшее время.</i>\n\n"
"<b>‼️ Чтобы убедиться, что вы общаетесь с доверенным контактом ШВЕЙТЕХЦЕНТР, перешлите любое сообщение человека в этот чат.</b>",
reply_markup=return_menu,
)
cart = await order_service.build_full_cart_dto(session, cb.from_user.id)
try:
await cb.bot.send_message(
NOTIFICATION_CHANNEL,
"🟢 Новый заказ.\n\n"
"━━━━━━━━━━━━━━\n\n"
f"<i>👤 Имя: {ctx.name}</i>\n"
f"<i>📱 Номер телефона: {ctx.phone}</i>\n"
f"<i>📍 Адрес: {ctx.address}</i>\n\n"
"━━━━━━━━━━━━━━\n"
f"{get_order_item_list(cart)}",
reply_markup=customer_contacts(
cb.from_user.id, bool(cb.from_user.username), cb.from_user.username
),
)
except Exception as e:
logger.exception(e)
return

View File

@@ -0,0 +1,29 @@
import logging
from aiogram import Router
from aiogram.filters import CommandObject, CommandStart
from aiogram.types import (
Message,
)
from sqlalchemy.ext.asyncio import AsyncSession
from repositories.invoices import InvoiceRepository
router = Router()
logger = logging.getLogger(__name__)
@router.message(CommandStart(deep_link=True, deep_link_encoded=True))
async def activate_invoice(
msg: Message,
command: CommandObject,
session: AsyncSession,
invoice_repo: InvoiceRepository,
):
payload = command.args
invoice_id = int(payload.split(":")[1])
invoice = await invoice_repo.get_invoice_by_id(session, invoice_id)
await msg.answer(
f"<b>🟢 Получен счёт на {invoice.amount}₽</b>"
) # TODO: Process payment

View File

@@ -5,6 +5,7 @@ from aiogram.fsm.context import FSMContext
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
from repositories.order_items import OrderItemRepository
from services.orders import OrderService from services.orders import OrderService
router = Router() router = Router()
@@ -12,11 +13,16 @@ router = Router()
@router.message(Command("start")) @router.message(Command("start"))
async def main_menu( async def main_menu(
msg: Message, state: FSMContext, session: AsyncSession, order_service: OrderService msg: Message,
state: FSMContext,
session: AsyncSession,
order_items_repo: OrderItemRepository,
): ):
await state.clear() await state.clear()
cart_items = await order_service.get_cart_items_count(session, msg.from_user.id) cart_items = await order_items_repo.get_items_count_by_customer(
session, msg.from_user.id
)
await msg.answer( await msg.answer(
"hii!", reply_markup=main_menu_kb(cart_items) "hii!", reply_markup=main_menu_kb(cart_items)
) # TODO: Write a welcome message ) # TODO: Write a welcome message
@@ -27,11 +33,13 @@ async def main_menu_cb(
cb: CallbackQuery, cb: CallbackQuery,
state: FSMContext, state: FSMContext,
session: AsyncSession, session: AsyncSession,
order_service: OrderService, order_items_repo: OrderService,
): ):
await state.clear() await state.clear()
cart_items = await order_service.get_cart_items_count(session, cb.from_user.id) cart_items = await order_items_repo.get_items_count_by_customer(
session, cb.from_user.id
)
await cb.message.edit_text( await cb.message.edit_text(
"hii!", reply_markup=main_menu_kb(cart_items) "hii!", reply_markup=main_menu_kb(cart_items)
) # TODO: Write a welcome message ) # TODO: Write a welcome message

21
main.py
View File

@@ -11,6 +11,7 @@ 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
from repositories.categories import CategoriesRepository from repositories.categories import CategoriesRepository
from repositories.invoices import InvoiceRepository
from repositories.order_items import OrderItemRepository 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
@@ -38,26 +39,18 @@ async def main():
product_repository = ProductRepository() product_repository = ProductRepository()
# DI Middleware Init invoice_repository = InvoiceRepository()
dp.message.middleware(DIMiddleware(order_service))
dp.message.middleware(DBSessionMiddleware(async_session))
dp.message.middleware(
RepositoryMiddleware(
order_repository,
category_repository,
product_repository,
order_items_repository,
)
)
dp.callback_query.middleware(DIMiddleware(order_service)) # DI Middleware Init
dp.callback_query.middleware(DBSessionMiddleware(async_session)) dp.update.middleware(DIMiddleware(order_service))
dp.callback_query.middleware( dp.update.middleware(DBSessionMiddleware(async_session))
dp.update.middleware(
RepositoryMiddleware( RepositoryMiddleware(
order_repository, order_repository,
category_repository, category_repository,
product_repository, product_repository,
order_items_repository, order_items_repository,
invoice_repository,
) )
) )

View File

@@ -1,6 +1,7 @@
from aiogram import BaseMiddleware from aiogram import BaseMiddleware
from repositories import OrderRepository, CategoriesRepository from repositories import OrderRepository, CategoriesRepository
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
@@ -12,15 +13,19 @@ class RepositoryMiddleware(BaseMiddleware):
categories_repo: CategoriesRepository, categories_repo: CategoriesRepository,
products_repo: ProductRepository, products_repo: ProductRepository,
order_items_repo: OrderItemRepository, order_items_repo: OrderItemRepository,
invoice_repo: InvoiceRepository,
) -> None: ) -> None:
self._order_repo = order_repo self._order_repo = order_repo
self._categories_repo = categories_repo self._categories_repo = categories_repo
self._products_repo = products_repo self._products_repo = products_repo
self._order_items_repo = order_items_repo self._order_items_repo = order_items_repo
self._invoice_repo = invoice_repo
async def __call__(self, handler, event, data): async def __call__(self, handler, event, data):
data["order_repo"] = self._order_repo data["order_repo"] = self._order_repo
data["categories_repo"] = self._categories_repo data["categories_repo"] = self._categories_repo
data["products_repo"] = self._products_repo data["products_repo"] = self._products_repo
data["order_items_repo"] = self._order_items_repo data["order_items_repo"] = self._order_items_repo
data["invoice_repo"] = self._invoice_repo
return await handler(event, data) return await handler(event, data)

9
misc/filters.py Normal file
View File

@@ -0,0 +1,9 @@
from aiogram.filters import BaseFilter
from aiogram.types import InlineQuery
from config import VERIFIED_ACCOUNTS
class IsVerified(BaseFilter):
async def __call__(self, inline_query: InlineQuery) -> bool:
return inline_query.from_user.id in VERIFIED_ACCOUNTS

View File

@@ -0,0 +1,34 @@
from typing import Optional
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
placeholder_kb = InlineKeyboardBuilder(
[[InlineKeyboardButton(text="🍃", callback_data="...")]]
).as_markup()
def customer_contacts(
user_id: int, has_mention: bool, mention: Optional[str] = None
) -> InlineKeyboardMarkup:
btns: list[list[InlineKeyboardButton]] = []
(
btns.append(
[InlineKeyboardButton(text="👤 Перейти", url=f"https://t.me/{mention}")]
)
if has_mention
else None
)
btns.append(
[
InlineKeyboardButton(text="✍️", url=f"tg://openmessage?user_id={user_id}"),
InlineKeyboardButton(text="✍️", url=f"tg://user?id={user_id}"),
]
)
return InlineKeyboardBuilder(btns).as_markup()
def payment_link(url: str) -> InlineKeyboardMarkup:
return InlineKeyboardBuilder(
[[InlineKeyboardButton(text="💸 Оплатить", url=url)]]
).as_markup()

View File

@@ -214,7 +214,7 @@ def order_specs_confirmation(order_id: int) -> InlineKeyboardMarkup:
[ [
[ [
InlineKeyboardButton( InlineKeyboardButton(
text="🟢 Перейти к оплате", text="🟢 Подтвердить",
callback_data=f"payment:{order_id}", callback_data=f"payment:{order_id}",
) )
], ],

View File

@@ -5,3 +5,8 @@ class CheckoutStorage(StatesGroup):
name = State() name = State()
phone = State() phone = State()
address = State() address = State()
confirmation = State()
class PaymentStorage(StatesGroup):
refresh = State()

View File

@@ -18,8 +18,3 @@ def get_order_item_list(cart: CartDTO):
text += f"\n━━━━━━━━━━━━━━\n💰 Итого: {cart.total}" text += f"\n━━━━━━━━━━━━━━\n💰 Итого: {cart.total}"
return text return text
# def get_order_notification(cart: CartDTO):
# text
# FIXME

View File

@@ -1,3 +1,6 @@
import base64
import json
from typing import Any
import phonenumbers import phonenumbers
from phonenumbers import NumberParseException from phonenumbers import NumberParseException
@@ -8,3 +11,17 @@ def is_valid_phone(phone: str, region="RU") -> bool:
return phonenumbers.is_valid_number(parsed) return phonenumbers.is_valid_number(parsed)
except NumberParseException: except NumberParseException:
return False return False
def dict_to_b64(payload: dict) -> str:
json_str = json.dumps(payload, separators=(",", ":"))
json_bytes = json_str.encode("utf-8")
b64 = base64.urlsafe_b64encode(json_bytes).decode("ascii")
return b64.rstrip("=")
def b64_to_dict(payload: str) -> dict["str", Any]:
padding = "=" * (-len(payload) % 4)
payload += padding
json_bytes = base64.urlsafe_b64decode(payload)
return json.loads(json_bytes.decode("utf-8"))

View File

@@ -1 +0,0 @@
docker run --name test-postgres -e POSTGRES_PASSWORD=HEXDEVFUCKINGSUCKS! -p 5432:5432 -d postgres

View File

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

45
repositories/invoices.py Normal file
View File

@@ -0,0 +1,45 @@
from typing import Optional
from sqlalchemy import select, update
from db.models import Invoice
from sqlalchemy.ext.asyncio import AsyncSession
from db.models.invoices import InvoiceStatus
class InvoiceRepository:
async def create_invoice(
self,
session: AsyncSession,
*,
amount: int,
creator_id: int,
inline_message_id: Optional[int] = None,
status: InvoiceStatus = InvoiceStatus.PENDING,
) -> Invoice:
invoice = Invoice(
creator_id=creator_id,
amount=amount,
status=status,
inline_message_id=inline_message_id,
)
session.add(invoice)
await session.commit()
return invoice
async def get_invoice_by_id(
self, session: AsyncSession, invoice_id: int
) -> Optional[Invoice]:
stmt = select(Invoice).where(Invoice.id == invoice_id)
return await session.scalar(stmt)
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)

View File

@@ -2,7 +2,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import delete, func, select, update from sqlalchemy import delete, func, select, update
from typing import Optional from typing import Optional
from db.models.orders import OrderItem from db.models.orders import Order, OrderItem, OrderStatus
class OrderItemRepository: class OrderItemRepository:
@@ -29,6 +29,19 @@ 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(
self, session: AsyncSession, customer: int
) -> int:
count = await session.scalar(
select(func.count(OrderItem.id))
.join(Order)
.where(
Order.customer == customer,
Order.status == OrderStatus.DRAFT,
)
)
return int(count)
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]: ) -> Optional[OrderItem]:

View File

@@ -5,3 +5,5 @@ black
asyncpg asyncpg
sqlalchemy[asyncio] sqlalchemy[asyncio]
psycopg2 psycopg2
phonenumbers
ruff

View File

@@ -149,13 +149,6 @@ class OrderService:
session, order_item_id=order_item.id, quantity=0 session, order_item_id=order_item.id, quantity=0
) )
async def get_cart_items_count(self, session: AsyncSession, customer: int) -> int:
order = await self.order_repo.get_draft_order_by_user(session, customer)
if not order:
return 0
return await self.order_items_repo.get_items_count(session, order_id=order.id)
async def build_pagination_cart_dto( async def build_pagination_cart_dto(
self, self,
session: AsyncSession, session: AsyncSession,