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:
201
app/api/v1/vendor/invoices.py
vendored
201
app/api/v1/vendor/invoices.py
vendored
@@ -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}"'
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user