invix_test_task/api/app/services.py

220 lines
7.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()