162 lines
5.2 KiB
Python
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()
|