from typing import List from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile from fastapi.responses import FileResponse from sqlalchemy.orm import Session from backend.app.db import get_db from backend.app import models from backend.app.schemas import ( FinanceRecordRead, FinanceRecordUpdate, FinanceSyncResponse, FinanceSyncResult, FinanceUploadResponse, ) from backend.app.services.email_service import create_monthly_zip, sync_finance_emails from backend.app.services.invoice_upload import process_invoice_upload router = APIRouter(prefix="/finance", tags=["finance"]) @router.post("/sync", response_model=FinanceSyncResponse) async def sync_finance(): try: items_raw = await sync_finance_emails() except RuntimeError as exc: # 邮箱配置/连接等问题属于可预期的业务错误,用 400 让前端直接展示原因,而不是泛化为 500。 raise HTTPException(status_code=400, detail=str(exc)) from exc details = [FinanceSyncResult(**item) for item in items_raw] return FinanceSyncResponse( status="success", new_files=len(details), details=details, ) @router.get("/months", response_model=List[str]) async def list_finance_months(db: Session = Depends(get_db)): """List distinct months that have finance records (YYYY-MM), newest first.""" from sqlalchemy import distinct rows = ( db.query(distinct(models.FinanceRecord.month)) .order_by(models.FinanceRecord.month.desc()) .all() ) return [r[0] for r in rows] @router.get("/records", response_model=List[FinanceRecordRead]) async def list_finance_records( month: str = Query(..., description="YYYY-MM"), db: Session = Depends(get_db), ): """List finance records for a given month.""" records = ( db.query(models.FinanceRecord) .filter(models.FinanceRecord.month == month) .order_by(models.FinanceRecord.created_at.desc()) .all() ) return records @router.post("/upload", response_model=FinanceUploadResponse, status_code=201) async def upload_invoice( file: UploadFile = File(...), db: Session = Depends(get_db), ): """Upload an invoice (PDF or image). Saves to data/finance/{YYYY-MM}/manual/, runs AI OCR for amount/date.""" suf = (file.filename or "").lower().split(".")[-1] if "." in (file.filename or "") else "" allowed = {"pdf", "jpg", "jpeg", "png", "webp"} if suf not in allowed: raise HTTPException(400, "仅支持 PDF、JPG、PNG、WEBP 格式") file_name, file_path, month_str, amount, billing_date = await process_invoice_upload(file) record = models.FinanceRecord( month=month_str, type="manual", file_name=file_name, file_path=file_path, amount=amount, billing_date=billing_date, ) db.add(record) db.commit() db.refresh(record) return record @router.patch("/records/{record_id}", response_model=FinanceRecordRead) async def update_finance_record( record_id: int, payload: FinanceRecordUpdate, db: Session = Depends(get_db), ): """Update amount and/or billing_date of a finance record (e.g. after manual review).""" record = db.query(models.FinanceRecord).get(record_id) if not record: raise HTTPException(404, "记录不存在") if payload.amount is not None: record.amount = payload.amount if payload.billing_date is not None: record.billing_date = payload.billing_date db.commit() db.refresh(record) return record @router.get("/download/{month}") async def download_finance_month(month: str): """ Download a zipped archive for a given month (YYYY-MM). """ try: zip_path = await create_monthly_zip(month) except FileNotFoundError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc return FileResponse( path=zip_path, media_type="application/zip", filename=f"finance_{month}.zip", )