"""Billing router — Invoice CRUD + PDF.""" from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import RedirectResponse from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.utils.auth import require_admin_or_pm from app.domains.billing.schemas import InvoiceCreate, InvoiceOut, InvoiceStatusUpdate from app.domains.billing.service import ( create_invoice, get_invoices, get_invoice, update_invoice_status, delete_invoice, render_pdf, ) # Keep the old pricing router re-export for backward compat from app.api.routers.pricing import router as pricing_router invoice_router = APIRouter(prefix="/billing", tags=["billing"]) @invoice_router.get("/invoices", response_model=list[InvoiceOut]) async def list_invoices( skip: int = 0, limit: int = 50, db: AsyncSession = Depends(get_db), current_user=Depends(require_admin_or_pm), ): return await get_invoices(db, skip=skip, limit=limit) @invoice_router.post("/invoices", response_model=InvoiceOut, status_code=status.HTTP_201_CREATED) async def create_invoice_endpoint( body: InvoiceCreate, db: AsyncSession = Depends(get_db), current_user=Depends(require_admin_or_pm), ): tenant_id = getattr(current_user, 'tenant_id', None) return await create_invoice( db, tenant_id=tenant_id, order_line_ids=body.order_line_ids, notes=body.notes, issued_at=body.issued_at, due_at=body.due_at, vat_rate=body.vat_rate, currency=body.currency, ) @invoice_router.get("/invoices/{invoice_id}", response_model=InvoiceOut) async def get_invoice_endpoint( invoice_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user=Depends(require_admin_or_pm), ): inv = await get_invoice(db, invoice_id) if not inv: raise HTTPException(status_code=404, detail="Invoice not found") return inv @invoice_router.patch("/invoices/{invoice_id}", response_model=InvoiceOut) async def update_invoice_status_endpoint( invoice_id: uuid.UUID, body: InvoiceStatusUpdate, db: AsyncSession = Depends(get_db), current_user=Depends(require_admin_or_pm), ): inv = await update_invoice_status(db, invoice_id, body.status) if not inv: raise HTTPException(status_code=404, detail="Invoice not found") return inv @invoice_router.get("/invoices/{invoice_id}/pdf") async def download_invoice_pdf( invoice_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user=Depends(require_admin_or_pm), ): key = await render_pdf(db, invoice_id) if not key: raise HTTPException(status_code=503, detail="PDF generation unavailable (WeasyPrint not installed)") from app.core.storage import get_storage url = get_storage().get_url(key) return RedirectResponse(url=url) @invoice_router.delete("/invoices/{invoice_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_invoice_endpoint( invoice_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user=Depends(require_admin_or_pm), ): ok = await delete_invoice(db, invoice_id) if not ok: raise HTTPException(status_code=400, detail="Only draft invoices can be deleted") __all__ = ["invoice_router", "pricing_router"]