mitlist/be/app/api/v1/endpoints/costs.py
Mohamad 29e42b8d80 feat: Enhance cost management features with new endpoints and services
This commit introduces significant updates to the cost management functionality, including:

- New endpoints for retrieving cost summaries and generating expenses from lists, improving user interaction with financial data.
- Implementation of a service layer for cost-related logic, encapsulating the core business rules for calculating cost summaries and managing expenses.
- Introduction of a financial activity endpoint to consolidate user expenses and settlements, enhancing the user experience by providing a comprehensive view of financial activities.
- Refactoring of existing code to improve maintainability and clarity, including the addition of new exception handling for financial conflicts.

These changes aim to streamline cost management processes and enhance the overall functionality of the application.
2025-06-21 00:53:03 +02:00

126 lines
5.4 KiB
Python

import logging
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_transactional_session
from app.auth import current_active_user
from app.models import User as UserModel, Expense as ExpenseModel
from app.schemas.cost import ListCostSummary, GroupBalanceSummary
from app.schemas.expense import ExpensePublic
from app.services import costs_service
from app.core.exceptions import (
ListNotFoundError,
ListPermissionError,
GroupNotFoundError,
GroupPermissionError,
InvalidOperationError
)
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 not found"},
},
)
async def get_list_cost_summary(
list_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""
Retrieves a calculated cost summary for a specific list.
If an expense has been generated for this list, the summary will be based on that.
Otherwise, it will be a basic summary of item prices.
This endpoint is idempotent and does not create any data.
"""
logger.info(f"User {current_user.email} requesting cost summary for list {list_id}")
try:
return await costs_service.get_list_cost_summary_logic(
db=db, list_id=list_id, current_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 HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))
except ListNotFoundError as e:
logger.warning(f"List {list_id} not found when getting cost summary: {str(e)}")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
@router.post(
"/lists/{list_id}/cost-summary",
response_model=ExpensePublic,
status_code=status.HTTP_201_CREATED,
summary="Generate and Get Expense from List Summary",
tags=["Costs"],
responses={
status.HTTP_403_FORBIDDEN: {"description": "User does not have permission to access this list"},
status.HTTP_404_NOT_FOUND: {"description": "List not found"},
status.HTTP_400_BAD_REQUEST: {"description": "Invalid operation (e.g., no items to expense, or expense already exists)"},
},
)
async def generate_expense_from_list_summary(
list_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""
Creates an ITEM_BASED expense from the items in a given list.
This should be called to finalize the costs for a shopping list and turn it into a formal expense.
It will fail if an expense for this list already exists.
"""
logger.info(f"User {current_user.email} requesting to generate expense from list {list_id}")
try:
expense = await costs_service.generate_expense_from_list_logic(
db=db, list_id=list_id, current_user_id=current_user.id
)
return expense
except (ListPermissionError, GroupPermissionError) as e:
logger.warning(f"Permission denied for user {current_user.email} on list {list_id}: {str(e)}")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))
except (ListNotFoundError, GroupNotFoundError) as e:
logger.warning(f"Resource not found for list {list_id}: {str(e)}")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except InvalidOperationError as e:
logger.warning(f"Invalid operation for list {list_id}: {str(e)}")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@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_transactional_session),
current_user: UserModel = Depends(current_active_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}")
try:
return await costs_service.get_group_balance_summary_logic(
db=db, group_id=group_id, current_user_id=current_user.id
)
except GroupPermissionError as e:
logger.warning(f"Permission denied for user {current_user.email} on group {group_id}: {str(e)}")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))
except GroupNotFoundError as e:
logger.warning(f"Group {group_id} not found when getting balance summary: {str(e)}")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))