# app/api/v1/shop/orders.py """ Shop Orders API (Customer authenticated) Endpoints for managing customer orders in shop frontend. Uses vendor from request.state (injected by VendorContextMiddleware). Requires customer authentication - get_current_customer_api validates that customer token vendor_id matches the URL vendor. Customer Context: get_current_customer_api returns Customer directly (not User), with vendor validation already performed. """ import logging from datetime import UTC, datetime from pathlib import Path as FilePath from fastapi import APIRouter, Depends, Path, Query, Request from fastapi.responses import FileResponse from sqlalchemy.orm import Session from app.api.deps import get_current_customer_api from app.core.database import get_db from app.exceptions import VendorNotFoundException from app.exceptions.invoice import InvoicePDFNotFoundException from app.services.cart_service import cart_service from app.services.email_service import EmailService from app.services.invoice_service import invoice_service from app.services.order_service import order_service from app.utils.money import cents_to_euros from models.database.customer import Customer from models.schema.order import ( OrderCreate, OrderDetailResponse, OrderListResponse, OrderResponse, ) router = APIRouter() logger = logging.getLogger(__name__) @router.post("/orders", response_model=OrderResponse) # authenticated def place_order( request: Request, order_data: OrderCreate, customer: Customer = Depends(get_current_customer_api), db: Session = Depends(get_db), ): """ Place a new order for current vendor. Vendor is automatically determined from request context. Customer must be authenticated to place an order. Creates an order from the customer's cart. Request Body: - Order data including shipping address, payment method, etc. """ # Get vendor from middleware (already validated by get_current_customer_api) vendor = getattr(request.state, "vendor", None) if not vendor: raise VendorNotFoundException("context", identifier_type="subdomain") logger.debug( f"[SHOP_API] place_order for customer {customer.id}", extra={ "vendor_id": vendor.id, "vendor_code": vendor.subdomain, "customer_id": customer.id, }, ) # Create order order = order_service.create_order( db=db, vendor_id=vendor.id, order_data=order_data ) db.commit() logger.info( f"Order {order.order_number} placed for vendor {vendor.subdomain}, " f"total: €{order.total_amount:.2f}", extra={ "order_id": order.id, "order_number": order.order_number, "customer_id": customer.id, "total_amount": float(order.total_amount), }, ) # Update customer stats customer.total_orders = (customer.total_orders or 0) + 1 customer.total_spent = (customer.total_spent or 0) + order.total_amount customer.last_order_date = datetime.now(UTC) db.flush() logger.debug( f"Updated customer stats: total_orders={customer.total_orders}, " f"total_spent={customer.total_spent}" ) # Clear cart (get session_id from request cookies or headers) session_id = request.cookies.get("cart_session_id") or request.headers.get( "X-Cart-Session-Id" ) if session_id: try: cart_service.clear_cart(db, vendor.id, session_id) logger.debug(f"Cleared cart for session {session_id}") except Exception as e: logger.warning(f"Failed to clear cart: {e}") # Send order confirmation email try: email_service = EmailService(db) email_service.send_template( template_code="order_confirmation", to_email=customer.email, to_name=customer.full_name, language=customer.preferred_language or "en", variables={ "customer_name": customer.first_name or customer.full_name, "order_number": order.order_number, "order_total": f"€{order.total_amount:.2f}", "order_items_count": len(order.items), "order_date": order.order_date.strftime("%d.%m.%Y") if order.order_date else "", "shipping_address": f"{order.ship_address_line_1}, {order.ship_postal_code} {order.ship_city}", }, vendor_id=vendor.id, related_type="order", related_id=order.id, ) logger.info(f"Sent order confirmation email to {customer.email}") except Exception as e: logger.warning(f"Failed to send order confirmation email: {e}") return OrderResponse.model_validate(order) @router.get("/orders", response_model=OrderListResponse) def get_my_orders( request: Request, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), customer: Customer = Depends(get_current_customer_api), db: Session = Depends(get_db), ): """ Get order history for authenticated customer. Vendor is automatically determined from request context. Returns all orders placed by the authenticated customer. Query Parameters: - skip: Number of orders to skip (pagination) - limit: Maximum number of orders to return """ # Get vendor from middleware (already validated by get_current_customer_api) vendor = getattr(request.state, "vendor", None) if not vendor: raise VendorNotFoundException("context", identifier_type="subdomain") logger.debug( f"[SHOP_API] get_my_orders for customer {customer.id}", extra={ "vendor_id": vendor.id, "vendor_code": vendor.subdomain, "customer_id": customer.id, "skip": skip, "limit": limit, }, ) # Get orders orders, total = order_service.get_customer_orders( db=db, vendor_id=vendor.id, customer_id=customer.id, skip=skip, limit=limit ) return OrderListResponse( orders=[OrderResponse.model_validate(o) for o in orders], total=total, skip=skip, limit=limit, ) @router.get("/orders/{order_id}", response_model=OrderDetailResponse) def get_order_details( request: Request, order_id: int = Path(..., description="Order ID", gt=0), customer: Customer = Depends(get_current_customer_api), db: Session = Depends(get_db), ): """ Get detailed order information for authenticated customer. Vendor is automatically determined from request context. Customer can only view their own orders. Path Parameters: - order_id: ID of the order to retrieve """ # Get vendor from middleware (already validated by get_current_customer_api) vendor = getattr(request.state, "vendor", None) if not vendor: raise VendorNotFoundException("context", identifier_type="subdomain") logger.debug( f"[SHOP_API] get_order_details: order {order_id}", extra={ "vendor_id": vendor.id, "vendor_code": vendor.subdomain, "customer_id": customer.id, "order_id": order_id, }, ) # Get order order = order_service.get_order(db=db, vendor_id=vendor.id, order_id=order_id) # Verify order belongs to customer if order.customer_id != customer.id: from app.exceptions import OrderNotFoundException raise OrderNotFoundException(str(order_id)) return OrderDetailResponse.model_validate(order) @router.get("/orders/{order_id}/invoice") def download_order_invoice( request: Request, order_id: int = Path(..., description="Order ID", gt=0), customer: Customer = Depends(get_current_customer_api), db: Session = Depends(get_db), ): """ Download invoice PDF for a customer's order. Vendor is automatically determined from request context. Customer can only download invoices for their own orders. Invoice is auto-generated if it doesn't exist. Path Parameters: - order_id: ID of the order to get invoice for """ from app.exceptions import OrderNotFoundException # Get vendor from middleware vendor = getattr(request.state, "vendor", None) if not vendor: raise VendorNotFoundException("context", identifier_type="subdomain") logger.debug( f"[SHOP_API] download_order_invoice: order {order_id}", extra={ "vendor_id": vendor.id, "vendor_code": vendor.subdomain, "customer_id": customer.id, "order_id": order_id, }, ) # Get order order = order_service.get_order(db=db, vendor_id=vendor.id, order_id=order_id) # Verify order belongs to customer if order.customer_id != customer.id: raise OrderNotFoundException(str(order_id)) # Only allow invoice download for orders that are at least processing allowed_statuses = ["processing", "partially_shipped", "shipped", "delivered", "completed"] if order.status not in allowed_statuses: from app.exceptions import ValidationException raise ValidationException("Invoice not available for pending orders") # Check if invoice exists for this order (via service layer) invoice = invoice_service.get_invoice_by_order_id( db=db, vendor_id=vendor.id, order_id=order_id ) # Create invoice if it doesn't exist if not invoice: logger.info(f"Creating invoice for order {order_id} (customer download)") invoice = invoice_service.create_invoice_from_order( db=db, vendor_id=vendor.id, order_id=order_id, ) db.commit() # Get or generate PDF pdf_path = invoice_service.get_pdf_path( db=db, vendor_id=vendor.id, invoice_id=invoice.id, ) if not pdf_path: # Generate PDF pdf_path = invoice_service.generate_pdf( db=db, vendor_id=vendor.id, invoice_id=invoice.id, ) # Verify file exists if not FilePath(pdf_path).exists(): raise InvoicePDFNotFoundException(invoice.id) filename = f"invoice-{invoice.invoice_number}.pdf" logger.info( f"Customer {customer.id} downloading invoice {invoice.invoice_number} for order {order.order_number}" ) return FileResponse( path=pdf_path, media_type="application/pdf", filename=filename, headers={ "Content-Disposition": f'attachment; filename="{filename}"' }, )