220 lines
7.4 KiB
Python
220 lines
7.4 KiB
Python
from decimal import Decimal
|
||
|
||
from fastapi import HTTPException
|
||
from sqlalchemy import func, select
|
||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.models import BotDialog, CartItem, Category, Order, OrderItem, Product
|
||
from app.schemas import (
|
||
CartItemOut,
|
||
CartOut,
|
||
CatalogOut,
|
||
CategoryOut,
|
||
DialogOut,
|
||
OrderItemOut,
|
||
OrderOut,
|
||
ProductOut,
|
||
money,
|
||
)
|
||
|
||
SORTS = {
|
||
"price_asc": (Product.price.asc(), Product.id.asc()),
|
||
"price_desc": (Product.price.desc(), Product.id.desc()),
|
||
"new": (Product.id.desc(),),
|
||
}
|
||
|
||
|
||
async def catalog(db: AsyncSession, *, category_id: int | None, sort: str, limit: int) -> CatalogOut:
|
||
categories = (await db.scalars(select(Category).order_by(Category.id))).all()
|
||
stmt = (
|
||
select(Product, Category.name)
|
||
.join(Category, Category.id == Product.category_id)
|
||
.where(Product.is_active.is_(True))
|
||
)
|
||
if category_id is not None:
|
||
stmt = stmt.where(Product.category_id == category_id)
|
||
stmt = stmt.order_by(*SORTS[sort]).limit(limit)
|
||
rows = (await db.execute(stmt)).all()
|
||
return CatalogOut(
|
||
categories=[CategoryOut.model_validate(item) for item in categories],
|
||
products=[_product_out(product, category_name) for product, category_name in rows],
|
||
)
|
||
|
||
|
||
async def product_detail(db: AsyncSession, product_id: int) -> tuple[Product, str]:
|
||
row = (
|
||
await db.execute(
|
||
select(Product, Category.name)
|
||
.join(Category, Category.id == Product.category_id)
|
||
.where(Product.id == product_id, Product.is_active.is_(True))
|
||
)
|
||
).one_or_none()
|
||
if row is None:
|
||
raise HTTPException(status_code=404, detail="Товар не найден")
|
||
return row
|
||
|
||
|
||
def _product_out(product: Product, category_name: str) -> ProductOut:
|
||
return ProductOut(
|
||
id=product.id,
|
||
category_id=product.category_id,
|
||
category_name=category_name,
|
||
title=product.title,
|
||
description=product.description,
|
||
price=product.price,
|
||
)
|
||
|
||
|
||
async def load_cart(db: AsyncSession, owner_key: str) -> CartOut:
|
||
rows = (
|
||
await db.execute(
|
||
select(CartItem, Product)
|
||
.join(Product, Product.id == CartItem.product_id)
|
||
.where(CartItem.owner_key == owner_key, Product.is_active.is_(True))
|
||
.order_by(CartItem.id)
|
||
)
|
||
).all()
|
||
items: list[CartItemOut] = []
|
||
total = Decimal("0.00")
|
||
for item, product in rows:
|
||
line = (product.price * item.qty).quantize(Decimal("0.01"))
|
||
total += line
|
||
items.append(
|
||
CartItemOut(
|
||
product_id=product.id,
|
||
title=product.title,
|
||
price=money(product.price),
|
||
qty=item.qty,
|
||
line_total=money(line),
|
||
)
|
||
)
|
||
return CartOut(items=items, total=money(total))
|
||
|
||
|
||
async def add_item(db: AsyncSession, *, owner_key: str, product_id: int, qty: int) -> CartOut:
|
||
product = await db.get(Product, product_id)
|
||
if product is None or not product.is_active:
|
||
raise HTTPException(status_code=404, detail="Товар не найден")
|
||
stmt = pg_insert(CartItem).values(owner_key=owner_key, product_id=product_id, qty=qty)
|
||
stmt = stmt.on_conflict_do_update(
|
||
constraint="uq_cart_owner_product",
|
||
set_={"qty": func.least(CartItem.qty + stmt.excluded.qty, 99)},
|
||
)
|
||
await db.execute(stmt)
|
||
return await load_cart(db, owner_key)
|
||
|
||
|
||
async def remove_item(db: AsyncSession, *, owner_key: str, product_id: int) -> CartOut:
|
||
item = await db.scalar(
|
||
select(CartItem).where(CartItem.owner_key == owner_key, CartItem.product_id == product_id)
|
||
)
|
||
if item is not None:
|
||
await db.delete(item)
|
||
await db.flush()
|
||
return await load_cart(db, owner_key)
|
||
|
||
|
||
async def checkout(db: AsyncSession, *, owner_key: str, customer_name: str, customer_phone: str) -> OrderOut:
|
||
rows = (
|
||
await db.execute(
|
||
select(CartItem, Product)
|
||
.join(Product, Product.id == CartItem.product_id)
|
||
.where(CartItem.owner_key == owner_key)
|
||
.order_by(CartItem.id)
|
||
.with_for_update(of=CartItem)
|
||
)
|
||
).all()
|
||
if not rows:
|
||
raise HTTPException(status_code=400, detail="Корзина пуста")
|
||
if any(not product.is_active for _, product in rows):
|
||
raise HTTPException(status_code=409, detail="В корзине есть недоступный товар")
|
||
|
||
total = sum((product.price * item.qty for item, product in rows), Decimal("0.00")).quantize(Decimal("0.01"))
|
||
order = Order(
|
||
owner_key=owner_key,
|
||
customer_name=customer_name,
|
||
customer_phone=customer_phone,
|
||
total=total,
|
||
)
|
||
db.add(order)
|
||
await db.flush()
|
||
item_rows: list[OrderItemOut] = []
|
||
for item, product in rows:
|
||
db.add(
|
||
OrderItem(
|
||
order_id=order.id,
|
||
product_id=product.id,
|
||
title=product.title,
|
||
price=product.price,
|
||
qty=item.qty,
|
||
)
|
||
)
|
||
item_rows.append(
|
||
OrderItemOut(
|
||
product_id=product.id,
|
||
title=product.title,
|
||
price=money(product.price),
|
||
qty=item.qty,
|
||
)
|
||
)
|
||
await db.delete(item)
|
||
await db.flush()
|
||
await db.refresh(order)
|
||
return OrderOut(
|
||
id=order.id,
|
||
status=order.status,
|
||
customer_name=order.customer_name,
|
||
customer_phone=order.customer_phone,
|
||
total=money(order.total),
|
||
created_at=order.created_at,
|
||
items=item_rows,
|
||
)
|
||
|
||
|
||
async def get_order(db: AsyncSession, *, owner_key: str, order_id: int) -> OrderOut:
|
||
order = await db.scalar(select(Order).where(Order.id == order_id, Order.owner_key == owner_key))
|
||
if order is None:
|
||
raise HTTPException(status_code=404, detail="Заказ не найден")
|
||
items = (
|
||
await db.scalars(select(OrderItem).where(OrderItem.order_id == order.id).order_by(OrderItem.id))
|
||
).all()
|
||
return OrderOut(
|
||
id=order.id,
|
||
status=order.status,
|
||
customer_name=order.customer_name,
|
||
customer_phone=order.customer_phone,
|
||
total=money(order.total),
|
||
created_at=order.created_at,
|
||
items=[
|
||
OrderItemOut(product_id=item.product_id, title=item.title, price=money(item.price), qty=item.qty)
|
||
for item in items
|
||
],
|
||
)
|
||
|
||
|
||
async def get_dialog(db: AsyncSession, telegram_user_id: int) -> DialogOut | None:
|
||
row = await db.get(BotDialog, telegram_user_id)
|
||
if row is None:
|
||
return None
|
||
return DialogOut.model_validate(row)
|
||
|
||
|
||
async def save_dialog(db: AsyncSession, telegram_user_id: int, step: str, name: str | None) -> DialogOut:
|
||
row = await db.get(BotDialog, telegram_user_id)
|
||
if row is None:
|
||
row = BotDialog(telegram_user_id=telegram_user_id, step=step, name=name)
|
||
db.add(row)
|
||
else:
|
||
row.step = step
|
||
row.name = name
|
||
await db.flush()
|
||
return DialogOut.model_validate(row)
|
||
|
||
|
||
async def clear_dialog(db: AsyncSession, telegram_user_id: int) -> None:
|
||
row = await db.get(BotDialog, telegram_user_id)
|
||
if row is not None:
|
||
await db.delete(row)
|
||
await db.flush()
|