Files
finance-app/backend/app/routers/accounts.py

162 lines
5.2 KiB
Python

from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from sqlmodel import Session, select
from ..database import get_session
from ..deps import get_current_user
from ..models import Account, AccountType, CsvUpload, User
from ..schemas.account import AccountCreate, AccountResponse, AccountUpdate
router = APIRouter(prefix="/api/accounts", tags=["accounts"])
def _serialize(account: Account) -> dict:
return AccountResponse.model_validate(account).model_dump(by_alias=True)
@router.get("")
def list_accounts(
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
):
accounts = session.exec(
select(Account).where(Account.user_id == current_user.id).order_by(Account.created_at)
).all()
return JSONResponse(content=jsonable_encoder([_serialize(a) for a in accounts]))
@router.post("", status_code=201)
def create_account(
body: AccountCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
):
try:
account_type = AccountType(body.type)
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid account type: {body.type}")
account = Account(
user_id=current_user.id,
name=body.name,
institution=body.institution,
type=account_type,
currency=body.currency,
)
session.add(account)
session.commit()
session.refresh(account)
return JSONResponse(content=jsonable_encoder(_serialize(account)), status_code=201)
@router.get("/{account_id}")
def get_account(
account_id: str,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
):
account = session.exec(
select(Account).where(Account.id == account_id, Account.user_id == current_user.id)
).first()
if not account:
raise HTTPException(status_code=404, detail="Not found")
return JSONResponse(content=jsonable_encoder(_serialize(account)))
@router.patch("/{account_id}")
def update_account(
account_id: str,
body: AccountUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
):
account = session.exec(
select(Account).where(Account.id == account_id, Account.user_id == current_user.id)
).first()
if not account:
raise HTTPException(status_code=404, detail="Not found")
updates = body.model_dump(exclude_unset=True, by_alias=False)
for field, value in updates.items():
setattr(account, field, value)
account.updated_at = datetime.utcnow()
session.add(account)
session.commit()
session.refresh(account)
return JSONResponse(content=jsonable_encoder(_serialize(account)))
@router.post("/{account_id}/record-value")
def record_investment_value(
account_id: str,
body: dict,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
):
from datetime import datetime as dt, timezone
account = session.exec(
select(Account).where(Account.id == account_id, Account.user_id == current_user.id, Account.type == AccountType.INVESTMENT)
).first()
if not account:
raise HTTPException(status_code=404, detail="Investment account not found")
value_cents = body.get("valueCents")
date_str = body.get("date", "")
if value_cents is None or not isinstance(value_cents, int):
raise HTTPException(status_code=400, detail="valueCents must be an integer")
if not date_str or len(date_str) != 10:
raise HTTPException(status_code=400, detail="date must be YYYY-MM-DD")
try:
d = dt.fromisoformat(date_str + "T12:00:00")
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date format")
year, month = d.year, d.month
account.current_balance_cents = value_cents
account.updated_at = dt.utcnow()
session.add(account)
from ..models import BalanceSnapshot
existing = session.exec(
select(BalanceSnapshot).where(
BalanceSnapshot.account_id == account_id,
BalanceSnapshot.year == year,
BalanceSnapshot.month == month,
)
).first()
if existing:
existing.balance_cents = value_cents
existing.computed_at = dt.utcnow()
session.add(existing)
else:
session.add(BalanceSnapshot(account_id=account_id, year=year, month=month, balance_cents=value_cents))
session.commit()
return JSONResponse(content={"ok": True})
@router.delete("/{account_id}", status_code=204)
def delete_account(
account_id: str,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
):
account = session.exec(
select(Account).where(Account.id == account_id, Account.user_id == current_user.id)
).first()
if not account:
raise HTTPException(status_code=404, detail="Not found")
# CsvUpload rows cascade via FK, but delete explicitly to match TS behavior
uploads = session.exec(select(CsvUpload).where(CsvUpload.account_id == account_id)).all()
for upload in uploads:
session.delete(upload)
session.delete(account)
session.commit()