"""Sale service: CRUD, contract PDF generation, GwG logic, USt-IdNr. verification.""" from __future__ import annotations import uuid from datetime import date from typing import Any from sqlalchemy import and_, func, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.config import settings from app.models.sale import Sale from app.models.vehicle import Vehicle from app.utils.contract_pdf import generate_contract_pdf async def create_sale(db: AsyncSession, data: dict[str, Any]) -> Sale: """Create a new sale record. Sets vehicle availability to 'sold' upon creation. Generates contract PDF if status is 'completed'. Raises ValueError if vehicle or buyer contact not found. """ vehicle_id = data["vehicle_id"] # Verify vehicle exists vehicle = await db.get(Vehicle, vehicle_id) if vehicle is None or vehicle.deleted_at is not None: raise ValueError(f"Vehicle {vehicle_id} not found") # Set sale_date to today if not provided if data.get("sale_date") is None: data["sale_date"] = date.today() sale = Sale(**data) db.add(sale) await db.flush() # Update vehicle status to sold vehicle.availability = "sold" await db.flush() # Load nested relationships for response await db.refresh(sale) # Explicitly load relationships stmt = ( select(Sale) .options( selectinload(Sale.vehicle), selectinload(Sale.buyer), selectinload(Sale.seller), ) .where(Sale.id == sale.id) ) result = await db.execute(stmt) sale = result.scalar_one() # Generate contract PDF if status is completed if sale.status == "completed": pdf_path = await generate_contract_pdf(sale) sale.contract_pdf_path = pdf_path await db.flush() return sale async def get_sale(db: AsyncSession, sale_id: uuid.UUID) -> Sale | None: """Get a single sale by ID with nested relationships.""" stmt = ( select(Sale) .options( selectinload(Sale.vehicle), selectinload(Sale.buyer), selectinload(Sale.seller), ) .where(Sale.id == sale_id) ) result = await db.execute(stmt) return result.scalar_one_or_none() async def list_sales( db: AsyncSession, page: int = 1, page_size: int = 20, status: str | None = None, date_from: date | None = None, date_to: date | None = None, ) -> tuple[list[Sale], int]: """List sales with pagination and optional filtering by status and date range. Returns (sales, total_count). """ conditions = [] if status: conditions.append(Sale.status == status) if date_from: conditions.append(Sale.sale_date >= date_from) if date_to: conditions.append(Sale.sale_date <= date_to) # Count query count_stmt = select(func.count(Sale.id)) if conditions: count_stmt = count_stmt.where(and_(*conditions)) total_result = await db.execute(count_stmt) total = total_result.scalar_one() # Data query with eager loading data_stmt = ( select(Sale) .options( selectinload(Sale.vehicle), selectinload(Sale.buyer), selectinload(Sale.seller), ) .order_by(Sale.created_at.desc()) ) if conditions: data_stmt = data_stmt.where(and_(*conditions)) offset = (page - 1) * page_size data_stmt = data_stmt.offset(offset).limit(page_size) result = await db.execute(data_stmt) sales = list(result.scalars().all()) return sales, total async def update_sale( db: AsyncSession, sale_id: uuid.UUID, updates: dict[str, Any] ) -> Sale | None: """Update a sale's fields. Returns None if not found.""" sale = await get_sale(db, sale_id) if sale is None: return None for key, value in updates.items(): if hasattr(sale, key): setattr(sale, key, value) await db.flush() # Reload with relationships sale = await get_sale(db, sale_id) return sale async def cancel_sale(db: AsyncSession, sale_id: uuid.UUID) -> Sale | None: """Cancel a sale: set status to 'cancelled' and restore vehicle status to 'available'. Returns None if sale not found. """ sale = await get_sale(db, sale_id) if sale is None: return None sale.status = "cancelled" # Restore vehicle availability vehicle = await db.get(Vehicle, sale.vehicle_id) if vehicle is not None: vehicle.availability = "available" await db.flush() sale = await get_sale(db, sale_id) return sale async def delete_sale(db: AsyncSession, sale_id: uuid.UUID) -> Sale | None: """Delete a sale (cancel + restore vehicle status to 'in_stock'). This is a soft-cancel: sale status is set to 'cancelled' and vehicle availability is restored to 'available' (in_stock). Returns None if sale not found. """ return await cancel_sale(db, sale_id) async def regenerate_contract_pdf(db: AsyncSession, sale_id: uuid.UUID) -> Sale | None: """Regenerate the contract PDF for a sale. Returns the updated sale with new contract_pdf_path, or None if not found. """ sale = await get_sale(db, sale_id) if sale is None: return None pdf_path = await generate_contract_pdf(sale) sale.contract_pdf_path = pdf_path await db.flush() sale = await get_sale(db, sale_id) return sale async def verify_ust_id(sale_id: uuid.UUID, db: AsyncSession) -> dict[str, Any]: """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'}. Args: sale_id: Sale ID to look up the buyer contact. db: Async session. Returns: Dict with verified, message, and vat_id fields. """ # Check feature flag bzst_enabled = getattr(settings, "BZST_API_ENABLED", False) if not bzst_enabled: return { "verified": False, "message": "BZSt API not available", "vat_id": None, } # If enabled, we would call the BZSt API here # For now, return manual review message sale = await get_sale(db, sale_id) if sale is None: return { "verified": False, "message": "Sale not found", "vat_id": None, } buyer = sale.buyer vat_id = getattr(buyer, "vat_id", None) if buyer else None return { "verified": False, "message": "Manual review required", "vat_id": vat_id, }