Обновление приложения
This commit is contained in:
163
backend/app/api/v1/operations.py
Normal file
163
backend/app/api/v1/operations.py
Normal file
@@ -0,0 +1,163 @@
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
from app.auth.dependencies import get_current_user
|
||||
from app.database import get_db
|
||||
from app.models.account import Account
|
||||
from app.models.operation import Operation, OperationType, OperationRelation
|
||||
from app.models.user import User
|
||||
from app.schemas.operation import (
|
||||
OperationCreate, OperationUpdate, OperationOut,
|
||||
OperationTypeOut, TransferCreate,
|
||||
OperationRelationOut,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/operations", tags=["operations"])
|
||||
|
||||
|
||||
def _assert_account_ownership(db: Session, account_id: int, user_id: int) -> Account:
|
||||
account = db.get(Account, account_id)
|
||||
if not account or account.user_id != user_id:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
return account
|
||||
|
||||
|
||||
def _apply_balance(account: Account, op_type_name: str, amount: Decimal, sign: int = 1):
|
||||
if op_type_name == "income":
|
||||
account.balance += amount * sign
|
||||
elif op_type_name == "expense":
|
||||
account.balance -= amount * sign
|
||||
|
||||
|
||||
@router.get("/types", response_model=list[OperationTypeOut])
|
||||
def list_types(db: Session = Depends(get_db), _=Depends(get_current_user)):
|
||||
return db.query(OperationType).all()
|
||||
|
||||
|
||||
@router.get("", response_model=list[OperationOut])
|
||||
def list_operations(
|
||||
account_id: int | None = None,
|
||||
category_id: int | None = None,
|
||||
operation_type_id: int | None = None,
|
||||
date_from: datetime | None = None,
|
||||
date_to: datetime | None = None,
|
||||
is_planned: bool | None = None,
|
||||
limit: int = Query(100, le=1000),
|
||||
offset: int = 0,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
q = db.query(Operation).filter(Operation.user_id == current_user.id)
|
||||
if account_id:
|
||||
q = q.filter(Operation.account_id == account_id)
|
||||
if category_id:
|
||||
q = q.filter(Operation.category_id == category_id)
|
||||
if operation_type_id:
|
||||
q = q.filter(Operation.operation_type_id == operation_type_id)
|
||||
if date_from:
|
||||
q = q.filter(Operation.date >= date_from)
|
||||
if date_to:
|
||||
q = q.filter(Operation.date <= date_to)
|
||||
if is_planned is not None:
|
||||
q = q.filter(Operation.is_planned == is_planned)
|
||||
return q.order_by(Operation.date.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
|
||||
@router.get("/{operation_id}", response_model=OperationOut)
|
||||
def get_operation(operation_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
obj = db.get(Operation, operation_id)
|
||||
if not obj or obj.user_id != current_user.id:
|
||||
raise HTTPException(status_code=404, detail="Operation not found")
|
||||
return obj
|
||||
|
||||
|
||||
@router.post("", response_model=OperationOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_operation(
|
||||
body: OperationCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
account = _assert_account_ownership(db, body.account_id, current_user.id)
|
||||
op_type = db.get(OperationType, body.operation_type_id)
|
||||
if not op_type:
|
||||
raise HTTPException(status_code=400, detail="Invalid operation type")
|
||||
obj = Operation(**body.model_dump(), user_id=current_user.id)
|
||||
db.add(obj)
|
||||
_apply_balance(account, op_type.name, body.amount)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@router.post("/transfer", response_model=list[OperationOut], status_code=status.HTTP_201_CREATED)
|
||||
def create_transfer(
|
||||
body: TransferCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
from_account = _assert_account_ownership(db, body.from_account_id, current_user.id)
|
||||
to_account = _assert_account_ownership(db, body.to_account_id, current_user.id)
|
||||
transfer_type = db.query(OperationType).filter(OperationType.name == "transfer").first()
|
||||
if not transfer_type:
|
||||
raise HTTPException(status_code=500, detail="Transfer operation type not configured")
|
||||
|
||||
op_out = Operation(
|
||||
user_id=current_user.id, account_id=body.from_account_id,
|
||||
operation_type_id=transfer_type.id, amount=body.amount,
|
||||
currency_id=body.currency_id, exchange_rate=body.exchange_rate,
|
||||
amount_converted=body.amount_converted, date=body.date,
|
||||
description=body.description, location_id=body.location_id,
|
||||
)
|
||||
amount_in = body.amount_converted or body.amount
|
||||
op_in = Operation(
|
||||
user_id=current_user.id, account_id=body.to_account_id,
|
||||
operation_type_id=transfer_type.id, amount=amount_in,
|
||||
currency_id=to_account.currency_id, date=body.date,
|
||||
description=body.description, location_id=body.location_id,
|
||||
)
|
||||
from_account.balance -= body.amount
|
||||
to_account.balance += amount_in
|
||||
db.add_all([op_out, op_in])
|
||||
db.flush()
|
||||
|
||||
relation = OperationRelation(
|
||||
operation_from_id=op_out.id, operation_to_id=op_in.id, relation_type="transfer"
|
||||
)
|
||||
db.add(relation)
|
||||
db.commit()
|
||||
db.refresh(op_out)
|
||||
db.refresh(op_in)
|
||||
return [op_out, op_in]
|
||||
|
||||
|
||||
@router.patch("/{operation_id}", response_model=OperationOut)
|
||||
def update_operation(
|
||||
operation_id: int, body: OperationUpdate,
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
obj = db.get(Operation, operation_id)
|
||||
if not obj or obj.user_id != current_user.id:
|
||||
raise HTTPException(status_code=404, detail="Operation not found")
|
||||
|
||||
if body.amount is not None and body.amount != obj.amount:
|
||||
account = db.get(Account, obj.account_id)
|
||||
op_type = db.get(OperationType, obj.operation_type_id)
|
||||
_apply_balance(account, op_type.name, obj.amount, sign=-1)
|
||||
_apply_balance(account, op_type.name, body.amount, sign=1)
|
||||
|
||||
for field, value in body.model_dump(exclude_none=True).items():
|
||||
setattr(obj, field, value)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@router.delete("/{operation_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_operation(
|
||||
operation_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
obj = db.get(Operation, operation_id)
|
||||
if not obj or obj.user_id != current_user.id:
|
||||
raise HTTPException(status_code=404, detail="Operation not found")
|
||||
account = db.get(Account, obj.account_id)
|
||||
op_type = db.get(OperationType, obj.operation_type_id)
|
||||
_apply_balance(account, op_type.name, obj.amount, sign=-1)
|
||||
db.delete(obj)
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user