128 lines
6.6 KiB
Python
128 lines
6.6 KiB
Python
# app/api/v1/endpoints/costs.py
|
|
import logging
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import Session, selectinload
|
|
|
|
from app.database import get_db
|
|
from app.api.dependencies import get_current_user
|
|
from app.models import User as UserModel, Group as GroupModel # For get_current_user dependency and Group model
|
|
from app.schemas.cost import ListCostSummary, GroupBalanceSummary
|
|
from app.crud import cost as crud_cost
|
|
from app.crud import list as crud_list # For permission checking
|
|
from app.core.exceptions import ListNotFoundError, ListPermissionError, UserNotFoundError, GroupNotFoundError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
@router.get(
|
|
"/lists/{list_id}/cost-summary",
|
|
response_model=ListCostSummary,
|
|
summary="Get Cost Summary for a List",
|
|
tags=["Costs"],
|
|
responses={
|
|
status.HTTP_403_FORBIDDEN: {"description": "User does not have permission to access this list"},
|
|
status.HTTP_404_NOT_FOUND: {"description": "List or associated user not found"}
|
|
}
|
|
)
|
|
async def get_list_cost_summary(
|
|
list_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Retrieves a calculated cost summary for a specific list, detailing total costs,
|
|
equal shares per user, and individual user balances based on their contributions.
|
|
|
|
The user must have access to the list to view its cost summary.
|
|
Costs are split among group members if the list belongs to a group, or just for
|
|
the creator if it's a personal list. All users who added items with prices are
|
|
included in the calculation.
|
|
"""
|
|
logger.info(f"User {current_user.email} requesting cost summary for list {list_id}")
|
|
|
|
# 1. Verify user has access to the target list
|
|
try:
|
|
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
|
|
except ListPermissionError as e:
|
|
logger.warning(f"Permission denied for user {current_user.email} on list {list_id}: {str(e)}")
|
|
raise # Re-raise the original exception to be handled by FastAPI
|
|
except ListNotFoundError as e:
|
|
logger.warning(f"List {list_id} not found when checking permissions for cost summary: {str(e)}")
|
|
raise # Re-raise
|
|
|
|
# 2. Calculate the cost summary
|
|
try:
|
|
cost_summary = await crud_cost.calculate_list_cost_summary(db=db, list_id=list_id)
|
|
logger.info(f"Successfully generated cost summary for list {list_id} for user {current_user.email}")
|
|
return cost_summary
|
|
except ListNotFoundError as e:
|
|
logger.warning(f"List {list_id} not found during cost summary calculation: {str(e)}")
|
|
# This might be redundant if check_list_permission already confirmed list existence,
|
|
# but calculate_list_cost_summary also fetches the list.
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
except UserNotFoundError as e:
|
|
logger.error(f"User not found during cost summary calculation for list {list_id}: {str(e)}")
|
|
# This indicates a data integrity issue (e.g., list creator or item adder missing)
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error generating cost summary for list {list_id} for user {current_user.email}: {str(e)}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred while generating the cost summary.")
|
|
|
|
@router.get(
|
|
"/groups/{group_id}/balance-summary",
|
|
response_model=GroupBalanceSummary,
|
|
summary="Get Detailed Balance Summary for a Group",
|
|
tags=["Costs", "Groups"],
|
|
responses={
|
|
status.HTTP_403_FORBIDDEN: {"description": "User does not have permission to access this group"},
|
|
status.HTTP_404_NOT_FOUND: {"description": "Group not found"}
|
|
}
|
|
)
|
|
async def get_group_balance_summary(
|
|
group_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Retrieves a detailed financial balance summary for all users within a specific group.
|
|
It considers all expenses, their splits, and all settlements recorded for the group.
|
|
The user must be a member of the group to view its balance summary.
|
|
"""
|
|
logger.info(f"User {current_user.email} requesting balance summary for group {group_id}")
|
|
|
|
# 1. Verify user is a member of the target group (using crud_group.check_group_membership or similar)
|
|
# Assuming a function like this exists in app.crud.group or we add it.
|
|
# For now, let's placeholder this check logic.
|
|
# await crud_group.check_group_membership(db=db, group_id=group_id, user_id=current_user.id)
|
|
# A simpler check for now: fetch the group and see if user is part of member_associations
|
|
group_check = await db.execute(
|
|
select(GroupModel)
|
|
.options(selectinload(GroupModel.member_associations))
|
|
.where(GroupModel.id == group_id)
|
|
)
|
|
db_group_for_check = group_check.scalars().first()
|
|
|
|
if not db_group_for_check:
|
|
raise GroupNotFoundError(group_id)
|
|
|
|
user_is_member = any(assoc.user_id == current_user.id for assoc in db_group_for_check.member_associations)
|
|
if not user_is_member:
|
|
# If ListPermissionError is generic enough for "access resource", use it, or a new GroupPermissionError
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"User not a member of group {group_id}")
|
|
|
|
# 2. Calculate the group balance summary
|
|
try:
|
|
balance_summary = await crud_cost.calculate_group_balance_summary(db=db, group_id=group_id)
|
|
logger.info(f"Successfully generated balance summary for group {group_id} for user {current_user.email}")
|
|
return balance_summary
|
|
except GroupNotFoundError as e:
|
|
logger.warning(f"Group {group_id} not found during balance summary calculation: {str(e)}")
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
except UserNotFoundError as e: # Should not happen if group members are correctly fetched
|
|
logger.error(f"User not found during balance summary for group {group_id}: {str(e)}")
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An internal error occurred finding a user for the summary.")
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error generating balance summary for group {group_id}: {str(e)}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred while generating the group balance summary.") |