from __future__ import annotations from fastapi import APIRouter, Depends, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy import select from sqlalchemy.orm import Session from app.cart import ( available_stock, build_cart_items, cart_count, get_cart_map, save_cart, ) from app.config import get_settings from app.database import get_db from app.models import Category, Order, Product from app.orders import OrderError, create_order_from_cart, pay_and_deliver router = APIRouter() settings = get_settings() def setup(templates: Jinja2Templates) -> None: router.templates = templates # type: ignore[attr-defined] def ctx(request: Request, **kwargs): data = { "request": request, "app_name": settings.app_name, "cart_count": cart_count(request), } data.update(kwargs) return data @router.get("/", response_class=HTMLResponse) def home(request: Request, db: Session = Depends(get_db)): categories = db.scalars( select(Category).order_by(Category.sort_order, Category.name) ).all() featured = db.scalars( select(Product) .where(Product.is_active.is_(True), Product.is_featured.is_(True)) .order_by(Product.id) .limit(4) ).all() products = db.scalars( select(Product) .where(Product.is_active.is_(True)) .order_by(Product.is_featured.desc(), Product.id) .limit(12) ).all() stock_map = { p.id: available_stock(db, p.id) for p in [*featured, *products] } return router.templates.TemplateResponse( # type: ignore[attr-defined] request, "index.html", ctx( request, categories=categories, featured=featured, products=products, stock_map=stock_map, ), ) @router.get("/product/{slug}", response_class=HTMLResponse) def product_page(slug: str, request: Request, db: Session = Depends(get_db)): product = db.scalar( select(Product).where(Product.slug == slug, Product.is_active.is_(True)) ) if not product: return RedirectResponse("/", status_code=303) stock = available_stock(db, product.id) return router.templates.TemplateResponse( # type: ignore[attr-defined] request, "product.html", ctx(request, product=product, stock=stock), ) @router.post("/cart/add/{product_id}") def cart_add( product_id: int, request: Request, quantity: int = Form(1), db: Session = Depends(get_db), ): product = db.scalar( select(Product).where(Product.id == product_id, Product.is_active.is_(True)) ) if not product: return RedirectResponse("/", status_code=303) qty = max(1, min(int(quantity or 1), 10)) stock = available_stock(db, product.id) cart = get_cart_map(request) current = cart.get(str(product_id), 0) cart[str(product_id)] = min(current + qty, max(stock, 0)) if cart[str(product_id)] <= 0: cart.pop(str(product_id), None) save_cart(request, cart) return RedirectResponse("/cart", status_code=303) @router.get("/cart", response_class=HTMLResponse) def cart_page(request: Request, db: Session = Depends(get_db)): items, total = build_cart_items(db, request) return router.templates.TemplateResponse( # type: ignore[attr-defined] request, "cart.html", ctx(request, items=items, total=total), ) @router.post("/cart/update") def cart_update( request: Request, product_id: int = Form(...), quantity: int = Form(...), db: Session = Depends(get_db), ): cart = get_cart_map(request) pid = str(product_id) qty = int(quantity) if qty <= 0: cart.pop(pid, None) else: stock = available_stock(db, product_id) cart[pid] = min(qty, max(stock, 0)) if cart[pid] <= 0: cart.pop(pid, None) save_cart(request, cart) return RedirectResponse("/cart", status_code=303) @router.post("/cart/remove/{product_id}") def cart_remove(product_id: int, request: Request): cart = get_cart_map(request) cart.pop(str(product_id), None) save_cart(request, cart) return RedirectResponse("/cart", status_code=303) @router.get("/checkout", response_class=HTMLResponse) def checkout_page(request: Request, db: Session = Depends(get_db)): items, total = build_cart_items(db, request) if not items: return RedirectResponse("/cart", status_code=303) return router.templates.TemplateResponse( # type: ignore[attr-defined] request, "checkout.html", ctx(request, items=items, total=total, error=None), ) @router.post("/checkout", response_class=HTMLResponse) def checkout_submit( request: Request, email: str = Form(...), note: str = Form(""), db: Session = Depends(get_db), ): items, total = build_cart_items(db, request) if not items: return RedirectResponse("/cart", status_code=303) cart = get_cart_map(request) try: order = create_order_from_cart( db, email=email, cart=cart, customer_note=note ) order = pay_and_deliver(db, order) save_cart(request, {}) return RedirectResponse(f"/order/{order.public_id}?email={order.email}", status_code=303) except OrderError as exc: return router.templates.TemplateResponse( # type: ignore[attr-defined] request, "checkout.html", ctx(request, items=items, total=total, error=str(exc)), status_code=400, ) @router.get("/order/{public_id}", response_class=HTMLResponse) def order_page( public_id: str, request: Request, email: str = "", db: Session = Depends(get_db), ): order = db.scalar(select(Order).where(Order.public_id == public_id.upper())) if not order: return RedirectResponse("/", status_code=303) if email and email.strip().lower() != order.email: return router.templates.TemplateResponse( # type: ignore[attr-defined] request, "order_locked.html", ctx(request, public_id=public_id, error="Email не совпадает с заказом"), ) if not email: return router.templates.TemplateResponse( # type: ignore[attr-defined] request, "order_locked.html", ctx(request, public_id=public_id, error=None), ) return router.templates.TemplateResponse( # type: ignore[attr-defined] request, "order.html", ctx(request, order=order), ) @router.post("/order/{public_id}", response_class=HTMLResponse) def order_unlock( public_id: str, request: Request, email: str = Form(...), ): return RedirectResponse( f"/order/{public_id}?email={email.strip().lower()}", status_code=303, ) @router.get("/lookup", response_class=HTMLResponse) def lookup_page(request: Request): return router.templates.TemplateResponse( # type: ignore[attr-defined] request, "lookup.html", ctx(request, error=None), ) @router.post("/lookup") def lookup_submit( request: Request, public_id: str = Form(...), email: str = Form(...), db: Session = Depends(get_db), ): order = db.scalar(select(Order).where(Order.public_id == public_id.strip().upper())) if not order or order.email != email.strip().lower(): return router.templates.TemplateResponse( # type: ignore[attr-defined] request, "lookup.html", ctx(request, error="Заказ не найден. Проверьте номер и email."), status_code=404, ) return RedirectResponse( f"/order/{order.public_id}?email={order.email}", status_code=303, )