# app/api/v1/vendor/invoices.py """ Vendor invoice management endpoints. Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern). The get_current_vendor_api dependency guarantees token_vendor_id is present. Endpoints: - GET /invoices - List vendor invoices - GET /invoices/{invoice_id} - Get invoice details - POST /invoices - Create invoice from order - PUT /invoices/{invoice_id}/status - Update invoice status - GET /invoices/{invoice_id}/pdf - Download invoice PDF - POST /invoices/{invoice_id}/pdf - Generate/regenerate invoice PDF - GET /invoices/settings - Get invoice settings - POST /invoices/settings - Create invoice settings - PUT /invoices/settings - Update invoice settings - GET /invoices/stats - Get invoice statistics """ import logging from pathlib import Path from fastapi import APIRouter, Depends, Query from fastapi.responses import FileResponse from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_api from app.core.database import get_db from app.exceptions.invoice import ( InvoiceNotFoundException, InvoicePDFGenerationException, InvoicePDFNotFoundException, InvoiceSettingsNotFoundException, ) from app.services.invoice_service import invoice_service from models.database.user import User from models.schema.invoice import ( InvoiceCreate, InvoiceListPaginatedResponse, InvoiceListResponse, InvoicePDFGeneratedResponse, InvoiceResponse, InvoiceStatsResponse, InvoiceStatusUpdate, VendorInvoiceSettingsCreate, VendorInvoiceSettingsResponse, VendorInvoiceSettingsUpdate, ) router = APIRouter(prefix="/invoices") logger = logging.getLogger(__name__) # ============================================================================ # Invoice Settings # ============================================================================ @router.get("/settings", response_model=VendorInvoiceSettingsResponse | None) def get_invoice_settings( current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get vendor invoice settings. Returns null if settings not yet configured. """ settings = invoice_service.get_settings(db, current_user.token_vendor_id) if settings: return VendorInvoiceSettingsResponse.model_validate(settings) return None @router.post("/settings", response_model=VendorInvoiceSettingsResponse, status_code=201) def create_invoice_settings( data: VendorInvoiceSettingsCreate, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Create vendor invoice settings. Required before creating invoices. Sets company details, VAT number, invoice numbering preferences, and payment info. """ settings = invoice_service.create_settings( db=db, vendor_id=current_user.token_vendor_id, data=data, ) return VendorInvoiceSettingsResponse.model_validate(settings) @router.put("/settings", response_model=VendorInvoiceSettingsResponse) def update_invoice_settings( data: VendorInvoiceSettingsUpdate, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Update vendor invoice settings. """ settings = invoice_service.update_settings( db=db, vendor_id=current_user.token_vendor_id, data=data, ) return VendorInvoiceSettingsResponse.model_validate(settings) # ============================================================================ # Invoice Statistics # ============================================================================ @router.get("/stats", response_model=InvoiceStatsResponse) def get_invoice_stats( current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get invoice statistics for the vendor. Returns: - total_invoices: Total number of invoices - total_revenue: Total revenue from issued/paid invoices (EUR) - draft_count: Number of draft invoices - paid_count: Number of paid invoices """ stats = invoice_service.get_invoice_stats(db, current_user.token_vendor_id) return InvoiceStatsResponse( total_invoices=stats.get("total_invoices", 0), total_revenue_cents=stats.get("total_revenue_cents", 0), draft_count=stats.get("draft_count", 0), issued_count=stats.get("issued_count", 0), paid_count=stats.get("paid_count", 0), cancelled_count=stats.get("cancelled_count", 0), ) # ============================================================================ # Invoice CRUD # ============================================================================ @router.get("", response_model=InvoiceListPaginatedResponse) def list_invoices( page: int = Query(1, ge=1, description="Page number"), per_page: int = Query(20, ge=1, le=100, description="Items per page"), status: str | None = Query(None, description="Filter by status"), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ List vendor invoices with pagination. Supports filtering by status: draft, issued, paid, cancelled """ invoices, total = invoice_service.list_invoices( db=db, vendor_id=current_user.token_vendor_id, status=status, page=page, per_page=per_page, ) # Build response with buyer name from snapshot items = [] for inv in invoices: buyer_name = inv.buyer_details.get("name") if inv.buyer_details else None items.append( InvoiceListResponse( id=inv.id, invoice_number=inv.invoice_number, invoice_date=inv.invoice_date, status=inv.status, currency=inv.currency, total_cents=inv.total_cents, order_id=inv.order_id, buyer_name=buyer_name, ) ) pages = (total + per_page - 1) // per_page if total > 0 else 1 return InvoiceListPaginatedResponse( items=items, total=total, page=page, per_page=per_page, pages=pages, ) @router.get("/{invoice_id}", response_model=InvoiceResponse) def get_invoice( invoice_id: int, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get invoice details by ID. """ invoice = invoice_service.get_invoice_or_raise( db=db, vendor_id=current_user.token_vendor_id, invoice_id=invoice_id, ) return InvoiceResponse.model_validate(invoice) @router.post("", response_model=InvoiceResponse, status_code=201) def create_invoice( data: InvoiceCreate, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Create an invoice from an order. Automatically: - Captures seller/buyer details snapshots - Calculates VAT based on buyer country and OSS settings - Generates invoice number """ invoice = invoice_service.create_invoice_from_order( db=db, vendor_id=current_user.token_vendor_id, order_id=data.order_id, notes=data.notes, ) return InvoiceResponse.model_validate(invoice) @router.put("/{invoice_id}/status", response_model=InvoiceResponse) def update_invoice_status( invoice_id: int, data: InvoiceStatusUpdate, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Update invoice status. Valid statuses: - draft: Invoice created but not yet sent - issued: Invoice sent to customer - paid: Payment received - cancelled: Invoice cancelled Note: Cancelled invoices cannot be modified. """ invoice = invoice_service.update_status( db=db, vendor_id=current_user.token_vendor_id, invoice_id=invoice_id, new_status=data.status, ) return InvoiceResponse.model_validate(invoice) # ============================================================================ # PDF Generation # ============================================================================ @router.post("/{invoice_id}/pdf", response_model=InvoicePDFGeneratedResponse) def generate_invoice_pdf( invoice_id: int, regenerate: bool = Query(False, description="Force regenerate if exists"), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Generate PDF for an invoice. Set regenerate=true to force regeneration if PDF already exists. Returns the path to the generated PDF. """ pdf_path = invoice_service.generate_pdf( db=db, vendor_id=current_user.token_vendor_id, invoice_id=invoice_id, force_regenerate=regenerate, ) return InvoicePDFGeneratedResponse(pdf_path=pdf_path) @router.get("/{invoice_id}/pdf") def download_invoice_pdf( invoice_id: int, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Download invoice PDF. Returns the PDF file for download. Generates PDF if not already generated. """ # Check if PDF exists, generate if not pdf_path = invoice_service.get_pdf_path( db=db, vendor_id=current_user.token_vendor_id, invoice_id=invoice_id, ) if not pdf_path: # Generate PDF pdf_path = invoice_service.generate_pdf( db=db, vendor_id=current_user.token_vendor_id, invoice_id=invoice_id, ) # Verify file exists if not Path(pdf_path).exists(): raise InvoicePDFNotFoundException(invoice_id) # Get invoice for filename invoice = invoice_service.get_invoice_or_raise( db=db, vendor_id=current_user.token_vendor_id, invoice_id=invoice_id, ) filename = f"invoice-{invoice.invoice_number}.pdf" return FileResponse( path=pdf_path, media_type="application/pdf", filename=filename, headers={ "Content-Disposition": f'attachment; filename="{filename}"' }, )