# app/modules/orders/routes/api/storefront.py """ Orders Module - Storefront API Routes Authenticated endpoints for customer order operations: - View order history - View order details - Download invoices Uses vendor from middleware context (VendorContextMiddleware). Requires customer authentication. """ import logging 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.modules.orders.exceptions import OrderNotFoundException from app.modules.tenancy.exceptions import VendorNotFoundException from app.modules.orders.exceptions import InvoicePDFNotFoundException from app.modules.customers.schemas import CustomerContext from app.modules.orders.services import order_service from app.modules.orders.services.invoice_service import invoice_service # noqa: MOD-004 - Core invoice service from app.modules.orders.schemas import ( OrderDetailResponse, OrderListResponse, OrderResponse, ) router = APIRouter() logger = logging.getLogger(__name__) @router.get("/orders", response_model=OrderListResponse) # authenticated def get_my_orders( request: Request, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), customer: CustomerContext = 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 """ vendor = getattr(request.state, "vendor", None) if not vendor: raise VendorNotFoundException("context", identifier_type="subdomain") logger.debug( f"[ORDERS_STOREFRONT] get_my_orders for customer {customer.id}", extra={ "vendor_id": vendor.id, "vendor_code": vendor.subdomain, "customer_id": customer.id, "skip": skip, "limit": limit, }, ) 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: CustomerContext = 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 """ vendor = getattr(request.state, "vendor", None) if not vendor: raise VendorNotFoundException("context", identifier_type="subdomain") logger.debug( f"[ORDERS_STOREFRONT] get_order_details: order {order_id}", extra={ "vendor_id": vendor.id, "vendor_code": vendor.subdomain, "customer_id": customer.id, "order_id": order_id, }, ) 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)) 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: CustomerContext = 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 ValidationException vendor = getattr(request.state, "vendor", None) if not vendor: raise VendorNotFoundException("context", identifier_type="subdomain") logger.debug( f"[ORDERS_STOREFRONT] download_order_invoice: order {order_id}", extra={ "vendor_id": vendor.id, "vendor_code": vendor.subdomain, "customer_id": customer.id, "order_id": order_id, }, ) 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: raise ValidationException("Invoice not available for pending orders") # Check if invoice exists for this order 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: 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}"'}, )