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:
megaproxy 2026-04-23 10:54:32 +00:00
parent 312594f3d2
commit 8ef3bb2965
9 changed files with 181 additions and 64 deletions

View file

@ -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():