fix: resolve architecture validation violations

- Add invoice exceptions module with proper exception hierarchy
- Replace HTTPException with service-layer exceptions in invoice API
- Add InvoicePDFGeneratedResponse and InvoiceStatsResponse Pydantic models
- Replace db.commit() with db.flush() in services for proper transaction control
- Update invoice service to use exceptions from app/exceptions/invoice.py

All 14 errors and 14 warnings are now resolved. Validation passes with
only INFO-level findings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-24 18:23:49 +01:00
parent 6232bb47f6
commit 319fba5d39
7 changed files with 276 additions and 147 deletions

View File

@@ -21,23 +21,27 @@ Endpoints:
import logging
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Query
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.services.invoice_service import (
from app.exceptions.invoice import (
InvoiceNotFoundException,
InvoicePDFGenerationException,
InvoicePDFNotFoundException,
InvoiceSettingsNotFoundException,
invoice_service,
)
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,
@@ -81,16 +85,12 @@ def create_invoice_settings(
Required before creating invoices. Sets company details,
VAT number, invoice numbering preferences, and payment info.
"""
try:
settings = invoice_service.create_settings(
db=db,
vendor_id=current_user.token_vendor_id,
data=data,
)
return VendorInvoiceSettingsResponse.model_validate(settings)
except Exception as e:
logger.error(f"Failed to create invoice settings: {e}")
raise HTTPException(status_code=400, detail=str(e))
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)
@@ -102,21 +102,12 @@ def update_invoice_settings(
"""
Update vendor invoice settings.
"""
try:
settings = invoice_service.update_settings(
db=db,
vendor_id=current_user.token_vendor_id,
data=data,
)
return VendorInvoiceSettingsResponse.model_validate(settings)
except InvoiceSettingsNotFoundException:
raise HTTPException(
status_code=404,
detail="Invoice settings not found. Create settings first.",
)
except Exception as e:
logger.error(f"Failed to update invoice settings: {e}")
raise HTTPException(status_code=400, detail=str(e))
settings = invoice_service.update_settings(
db=db,
vendor_id=current_user.token_vendor_id,
data=data,
)
return VendorInvoiceSettingsResponse.model_validate(settings)
# ============================================================================
@@ -124,7 +115,7 @@ def update_invoice_settings(
# ============================================================================
@router.get("/stats")
@router.get("/stats", response_model=InvoiceStatsResponse)
def get_invoice_stats(
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
@@ -138,7 +129,15 @@ def get_invoice_stats(
- draft_count: Number of draft invoices
- paid_count: Number of paid invoices
"""
return invoice_service.get_invoice_stats(db, current_user.token_vendor_id)
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),
)
# ============================================================================
@@ -204,15 +203,12 @@ def get_invoice(
"""
Get invoice details by ID.
"""
try:
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)
except InvoiceNotFoundException:
raise HTTPException(status_code=404, detail="Invoice not found")
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)
@@ -229,22 +225,13 @@ def create_invoice(
- Calculates VAT based on buyer country and OSS settings
- Generates invoice number
"""
try:
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)
except InvoiceSettingsNotFoundException:
raise HTTPException(
status_code=400,
detail="Invoice settings not configured. Set up invoice settings first.",
)
except Exception as e:
logger.error(f"Failed to create invoice: {e}")
raise HTTPException(status_code=400, detail=str(e))
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)
@@ -265,19 +252,13 @@ def update_invoice_status(
Note: Cancelled invoices cannot be modified.
"""
try:
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)
except InvoiceNotFoundException:
raise HTTPException(status_code=404, detail="Invoice not found")
except Exception as e:
logger.error(f"Failed to update invoice status: {e}")
raise HTTPException(status_code=400, detail=str(e))
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)
# ============================================================================
@@ -285,7 +266,7 @@ def update_invoice_status(
# ============================================================================
@router.post("/{invoice_id}/pdf")
@router.post("/{invoice_id}/pdf", response_model=InvoicePDFGeneratedResponse)
def generate_invoice_pdf(
invoice_id: int,
regenerate: bool = Query(False, description="Force regenerate if exists"),
@@ -298,19 +279,13 @@ def generate_invoice_pdf(
Set regenerate=true to force regeneration if PDF already exists.
Returns the path to the generated PDF.
"""
try:
pdf_path = invoice_service.generate_pdf(
db=db,
vendor_id=current_user.token_vendor_id,
invoice_id=invoice_id,
force_regenerate=regenerate,
)
return {"pdf_path": pdf_path, "message": "PDF generated successfully"}
except InvoiceNotFoundException:
raise HTTPException(status_code=404, detail="Invoice not found")
except Exception as e:
logger.error(f"Failed to generate PDF: {e}")
raise HTTPException(status_code=500, detail=f"Failed to generate PDF: {str(e)}")
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")
@@ -325,45 +300,39 @@ def download_invoice_pdf(
Returns the PDF file for download.
Generates PDF if not already generated.
"""
try:
# Check if PDF exists, generate if not
pdf_path = invoice_service.get_pdf_path(
# 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,
)
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)
# Verify file exists
if not Path(pdf_path).exists():
raise HTTPException(status_code=404, detail="PDF file not found")
# Get invoice for filename
invoice = invoice_service.get_invoice_or_raise(
db=db,
vendor_id=current_user.token_vendor_id,
invoice_id=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"
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}"'
},
)
except InvoiceNotFoundException:
raise HTTPException(status_code=404, detail="Invoice not found")
except Exception as e:
logger.error(f"Failed to download PDF: {e}")
raise HTTPException(status_code=500, detail=f"Failed to download PDF: {str(e)}")
return FileResponse(
path=pdf_path,
media_type="application/pdf",
filename=filename,
headers={
"Content-Disposition": f'attachment; filename="{filename}"'
},
)