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

@ -19,7 +19,7 @@ from app.schemas.auth import (
TOTPChallengeResponse,
TOTPLoginRequest,
TOTPSetupResponse,
TOTPVerifyRequest,
TokenResponse,
)
from app.services.auth_service import (
@ -297,20 +297,6 @@ async def totp_setup(
return TOTPSetupResponse(secret=secret, qr_code_png_b64=qr_b64, backup_codes=backup_codes)
@router.post("/totp/verify", status_code=200)
async def totp_verify(
body: TOTPVerifyRequest,
request: Request,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
# Secret must be passed back from setup — here we expect it stored temporarily in body
# In practice the client stores it until verification; it's never persisted until verified
# This endpoint receives the secret + verification code
# For simplicity we accept: {"secret": "...", "code": "..."}
# Redefine body inline:
raise HTTPException(status_code=400, detail="Use /totp/enable endpoint with secret and code")
@router.post("/totp/enable", status_code=200)
async def totp_enable(

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