Fix audit findings: budget editing, dead code, logging, multi-currency
- Add budget editing: updateBudget() API, edit button on budget cards, BudgetFormModal adapted for create/update (category locked on edit) - Remove permanently-broken POST /auth/totp/verify stub and its unused TOTPVerifyRequest schema - Wire getHoldingTransactions() to AssetDetail page — transaction history table now shows above the candlestick chart, sorted newest-first - Fix multi-currency net worth in account_service: account balances are now converted to base_currency via ExchangeRate table before summing - Replace silent bare pass exception handlers with logger.warning() in transactions.py (OCR/AI pipeline) and price_feed_service.py (search) — ValueError in date/number regex parsing left silent (control flow) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
312594f3d2
commit
8ef3bb2965
9 changed files with 181 additions and 64 deletions
|
|
@ -1,11 +1,14 @@
|
|||
import csv
|
||||
import io
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -265,8 +268,8 @@ async def delete_attachment(
|
|||
path = Path(settings.upload_dir) / str(user.id) / ref["stored_name"]
|
||||
try:
|
||||
path.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
except OSError as e:
|
||||
logger.warning("Could not delete attachment file %s: %s", path, e)
|
||||
|
||||
new_refs = [r for r in refs if r["id"] != attachment_id]
|
||||
await db.execute(
|
||||
|
|
@ -307,8 +310,8 @@ def _extract_ocr_text(file_bytes: bytes, mime_type: str) -> str:
|
|||
text = "\n".join(pages_text).strip()
|
||||
if text:
|
||||
return text
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning("pdfplumber text extraction failed: %s", e)
|
||||
# Scanned PDF — convert first page to image then OCR
|
||||
try:
|
||||
from pdf2image import convert_from_bytes
|
||||
|
|
@ -316,8 +319,8 @@ def _extract_ocr_text(file_bytes: bytes, mime_type: str) -> str:
|
|||
images = convert_from_bytes(file_bytes, first_page=1, last_page=1, dpi=200)
|
||||
if images:
|
||||
return pytesseract.image_to_string(images[0])
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning("pdf2image/tesseract OCR failed: %s", e)
|
||||
return ""
|
||||
else:
|
||||
import io
|
||||
|
|
@ -326,7 +329,8 @@ def _extract_ocr_text(file_bytes: bytes, mime_type: str) -> str:
|
|||
try:
|
||||
img = Image.open(io.BytesIO(file_bytes))
|
||||
return pytesseract.image_to_string(img)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning("Image OCR failed: %s", e)
|
||||
return ""
|
||||
|
||||
|
||||
|
|
@ -488,11 +492,10 @@ async def _call_ai_parse(file_bytes: bytes, mime_type: str, user_row) -> dict:
|
|||
"ocr_text": ocr_text,
|
||||
}
|
||||
except json.JSONDecodeError:
|
||||
# AI returned something non-JSON — fall through to rules, keep raw for debug
|
||||
pass
|
||||
logger.warning("AI returned non-JSON response, falling back to rule-based parser")
|
||||
|
||||
except (httpx.HTTPStatusError, httpx.RequestError):
|
||||
pass # fall through to rule-based
|
||||
except (httpx.HTTPStatusError, httpx.RequestError) as e:
|
||||
logger.warning("AI API request failed (%s), falling back to rule-based parser", type(e).__name__)
|
||||
|
||||
# Step 3: rule-based fallback (also used when AI is not configured)
|
||||
if ocr_text.strip():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue