from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlmodel import Session, col, select from app.auth import get_current_user from app.db import get_session from app.models.models import ( Category, Transaction, TransactionCreate, TransactionRead, TransactionSource, TransactionUpdate, ) router = APIRouter(prefix="/transactions", tags=["transactions"]) def auto_categorize(merchant: str, session: Session) -> Optional[int]: categories = session.exec(select(Category)).all() merchant_lower = merchant.lower() for cat in categories: if cat.auto_match_patterns: patterns = [p.strip().lower() for p in cat.auto_match_patterns.split(",")] if any(p in merchant_lower for p in patterns if p): return cat.id return None @router.get("/", response_model=list[TransactionRead]) def list_transactions( source: Optional[TransactionSource] = None, search: Optional[str] = None, category_id: Optional[int] = None, limit: int = Query(default=50, le=500), offset: int = 0, session: Session = Depends(get_session), _user: str = Depends(get_current_user), ): query = select(Transaction) if source: query = query.where(Transaction.source == source) if category_id: query = query.where(Transaction.category_id == category_id) if search: query = query.where(col(Transaction.merchant).ilike(f"%{search}%")) query = query.order_by(col(Transaction.date).desc()).offset(offset).limit(limit) return session.exec(query).all() @router.get("/recent", response_model=list[TransactionRead]) def recent_transactions( limit: int = Query(default=5, le=20), session: Session = Depends(get_session), _user: str = Depends(get_current_user), ): query = ( select(Transaction) .where(Transaction.source == TransactionSource.CREDIT_CARD) .order_by(col(Transaction.date).desc()) .limit(limit) ) return session.exec(query).all() @router.post("/", response_model=TransactionRead, status_code=201) def create_transaction( data: TransactionCreate, session: Session = Depends(get_session), _user: str = Depends(get_current_user), ): tx = Transaction.model_validate(data) if tx.category_id is None: tx.category_id = auto_categorize(tx.merchant, session) session.add(tx) session.commit() session.refresh(tx) return tx @router.patch("/{transaction_id}", response_model=TransactionRead) def update_transaction( transaction_id: int, data: TransactionUpdate, session: Session = Depends(get_session), _user: str = Depends(get_current_user), ): tx = session.get(Transaction, transaction_id) if not tx: raise HTTPException(status_code=404, detail="Transaction not found") update_data = data.model_dump(exclude_unset=True) for key, value in update_data.items(): setattr(tx, key, value) session.add(tx) session.commit() session.refresh(tx) return tx @router.delete("/{transaction_id}", status_code=204) def delete_transaction( transaction_id: int, session: Session = Depends(get_session), _user: str = Depends(get_current_user), ): tx = session.get(Transaction, transaction_id) if not tx: raise HTTPException(status_code=404, detail="Transaction not found") session.delete(tx) session.commit()