hku-class/backend/app/services/fund_service.py

135 lines
4.3 KiB
Python

from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.db.models import FundRecord, User
from app.schemas.fund import FundRecordCreate, FundRecordUpdate, FundStatistics
async def create_fund_record(
db: AsyncSession, class_id: int, recorder_id: int, data: FundRecordCreate
) -> FundRecord:
record = FundRecord(
class_id=class_id,
recorder_id=recorder_id,
type=data.type,
amount=data.amount,
category=data.category,
description=data.description,
image_urls=None,
record_date=data.record_date,
)
record.set_image_urls_list(data.image_urls or [])
db.add(record)
await db.commit()
await db.refresh(record)
return record
async def update_fund_record(
db: AsyncSession, record: FundRecord, data: FundRecordUpdate
) -> FundRecord:
values = data.model_dump(exclude_unset=True)
has_image_urls = "image_urls" in values
image_urls = values.pop("image_urls", None)
for field, value in values.items():
setattr(record, field, value)
if has_image_urls:
record.set_image_urls_list(image_urls or [])
await db.commit()
await db.refresh(record)
return record
async def delete_fund_record(db: AsyncSession, record: FundRecord):
await db.delete(record)
await db.commit()
async def get_fund_record_by_id(db: AsyncSession, record_id: int) -> FundRecord | None:
result = await db.execute(
select(FundRecord)
.options(selectinload(FundRecord.recorder))
.where(FundRecord.id == record_id)
)
return result.scalar_one_or_none()
async def list_fund_records(
db: AsyncSession,
class_id: int,
page: int = 1,
page_size: int = 20,
type: str | None = None,
category: str | None = None,
) -> tuple[list[FundRecord], int]:
query = select(FundRecord).where(FundRecord.class_id == class_id)
count_query = select(func.count(FundRecord.id)).where(FundRecord.class_id == class_id)
if type:
query = query.where(FundRecord.type == type)
count_query = count_query.where(FundRecord.type == type)
if category:
query = query.where(FundRecord.category == category)
count_query = count_query.where(FundRecord.category == category)
total_result = await db.execute(count_query)
total = total_result.scalar() or 0
result = await db.execute(
query.options(selectinload(FundRecord.recorder))
.order_by(FundRecord.record_date.desc(), FundRecord.created_at.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
records = list(result.scalars().all())
return records, total
async def get_fund_statistics(db: AsyncSession, class_id: int) -> FundStatistics:
# Total income
income_result = await db.execute(
select(func.coalesce(func.sum(FundRecord.amount), 0))
.where(FundRecord.class_id == class_id, FundRecord.type == "income")
)
total_income = float(income_result.scalar() or 0)
# Total expense
expense_result = await db.execute(
select(func.coalesce(func.sum(FundRecord.amount), 0))
.where(FundRecord.class_id == class_id, FundRecord.type == "expense")
)
total_expense = float(expense_result.scalar() or 0)
balance = total_income - total_expense
# Income by category
income_cat_result = await db.execute(
select(FundRecord.category, func.sum(FundRecord.amount))
.where(FundRecord.class_id == class_id, FundRecord.type == "income")
.group_by(FundRecord.category)
)
income_by_category = [
{"category": row[0], "amount": float(row[1] or 0)}
for row in income_cat_result.fetchall()
]
# Expense by category
expense_cat_result = await db.execute(
select(FundRecord.category, func.sum(FundRecord.amount))
.where(FundRecord.class_id == class_id, FundRecord.type == "expense")
.group_by(FundRecord.category)
)
expense_by_category = [
{"category": row[0], "amount": float(row[1] or 0)}
for row in expense_cat_result.fetchall()
]
return FundStatistics(
total_income=total_income,
total_expense=total_expense,
balance=balance,
income_by_category=income_by_category,
expense_by_category=expense_by_category,
)