"""Sales router: CRUD, contract PDF, USt-IdNr. verification.""" import os import uuid from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import get_current_user, get_pagination from app.models.user import User from app.schemas.sale import ( ContractResponse, SaleCreate, SaleListResponse, SaleResponse, SaleUpdate, UstIdVerifyResponse, ) from app.services import sale_service router = APIRouter(prefix="/sales", tags=["sales"]) @router.get("/", response_model=SaleListResponse, status_code=status.HTTP_200_OK) async def list_sales( db: AsyncSession = Depends(get_db), pagination: dict = Depends(get_pagination), status_filter: str | None = Query( None, alias="status", description="Filter by sale status" ), date_from: str | None = Query( None, description="Filter sales from this date (YYYY-MM-DD)" ), date_to: str | None = Query( None, description="Filter sales up to this date (YYYY-MM-DD)" ), current_user: User = Depends(get_current_user), ): """List sales with pagination, filtering by status and date range.""" from datetime import date as date_type parsed_date_from = None parsed_date_to = None if date_from: try: parsed_date_from = date_type.fromisoformat(date_from) except ValueError: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={ "error": { "code": "INVALID_DATE", "message": f"Invalid date_from format: {date_from}", } }, ) if date_to: try: parsed_date_to = date_type.fromisoformat(date_to) except ValueError: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={ "error": { "code": "INVALID_DATE", "message": f"Invalid date_to format: {date_to}", } }, ) sales, total = await sale_service.list_sales( db, page=pagination["page"], page_size=pagination["page_size"], status=status_filter, date_from=parsed_date_from, date_to=parsed_date_to, ) return SaleListResponse( items=[SaleResponse.model_validate(s) for s in sales], total=total, page=pagination["page"], page_size=pagination["page_size"], ) @router.post("/", response_model=SaleResponse, status_code=status.HTTP_201_CREATED) async def create_sale( body: SaleCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Create a new sale. Sets vehicle status to 'sold'.""" data = body.model_dump(exclude_unset=False) try: sale = await sale_service.create_sale(db, data) except ValueError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"error": {"code": "VEHICLE_NOT_FOUND", "message": str(exc)}}, ) return SaleResponse.model_validate(sale) @router.get("/{sale_id}", response_model=SaleResponse, status_code=status.HTTP_200_OK) async def get_sale( sale_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Get a single sale by ID with nested vehicle and contacts.""" sale = await sale_service.get_sale(db, sale_id) if sale is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"error": {"code": "SALE_NOT_FOUND", "message": "Sale not found"}}, ) return SaleResponse.model_validate(sale) @router.put("/{sale_id}", response_model=SaleResponse, status_code=status.HTTP_200_OK) async def update_sale( sale_id: uuid.UUID, body: SaleUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Update a sale's fields.""" updates = body.model_dump(exclude_unset=True) if not updates: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"error": {"code": "NO_FIELDS", "message": "No fields to update"}}, ) sale = await sale_service.update_sale(db, sale_id, updates) if sale is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"error": {"code": "SALE_NOT_FOUND", "message": "Sale not found"}}, ) return SaleResponse.model_validate(sale) @router.delete( "/{sale_id}", response_model=SaleResponse, status_code=status.HTTP_200_OK ) async def delete_sale( sale_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Cancel a sale (soft delete). Sets sale status to 'cancelled' and restores vehicle status to 'available'.""" sale = await sale_service.delete_sale(db, sale_id) if sale is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"error": {"code": "SALE_NOT_FOUND", "message": "Sale not found"}}, ) return SaleResponse.model_validate(sale) @router.post( "/{sale_id}/contract", response_model=ContractResponse, status_code=status.HTTP_200_OK, ) async def regenerate_contract( sale_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Regenerate the contract PDF for a sale.""" sale = await sale_service.regenerate_contract_pdf(db, sale_id) if sale is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"error": {"code": "SALE_NOT_FOUND", "message": "Sale not found"}}, ) return ContractResponse( sale_id=sale.id, contract_pdf_path=sale.contract_pdf_path or "", message="Contract regenerated", ) @router.get("/{sale_id}/contract", status_code=status.HTTP_200_OK) async def download_contract( sale_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Download the contract PDF for a sale.""" sale = await sale_service.get_sale(db, sale_id) if sale is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"error": {"code": "SALE_NOT_FOUND", "message": "Sale not found"}}, ) if not sale.contract_pdf_path or not os.path.exists(sale.contract_pdf_path): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={ "error": { "code": "CONTRACT_NOT_FOUND", "message": "Contract PDF not generated yet", } }, ) return FileResponse( path=sale.contract_pdf_path, media_type="application/pdf", filename=f"contract_{sale.id}.pdf", ) @router.post( "/{sale_id}/verify-ust-id", response_model=UstIdVerifyResponse, status_code=status.HTTP_200_OK, ) async def verify_ust_id( sale_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Verify the buyer's USt-IdNr. via BZSt API. Feature flag: BZST_API_ENABLED (default: False). When disabled, returns {verified: false, message: 'BZSt API not available'}. """ result = await sale_service.verify_ust_id(sale_id, db) return UstIdVerifyResponse(**result)