442 lines
23 KiB
Python
442 lines
23 KiB
Python
# app/api/v1/endpoints/financials.py
|
|
import logging
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query, Response
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from typing import List as PyList, Optional, Sequence
|
|
|
|
from app.database import get_db
|
|
from app.api.dependencies import get_current_user
|
|
from app.models import User as UserModel, Group as GroupModel, List as ListModel, UserGroup as UserGroupModel, UserRoleEnum
|
|
from app.schemas.expense import (
|
|
ExpenseCreate, ExpensePublic,
|
|
SettlementCreate, SettlementPublic,
|
|
ExpenseUpdate, SettlementUpdate
|
|
)
|
|
from app.crud import expense as crud_expense
|
|
from app.crud import settlement as crud_settlement
|
|
from app.crud import group as crud_group
|
|
from app.crud import list as crud_list
|
|
from app.core.exceptions import (
|
|
ListNotFoundError, GroupNotFoundError, UserNotFoundError,
|
|
InvalidOperationError, GroupPermissionError, ListPermissionError,
|
|
ItemNotFoundError, GroupMembershipError
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
# --- Helper for permissions ---
|
|
async def check_list_access_for_financials(db: AsyncSession, list_id: int, user_id: int, action: str = "access financial data for"):
|
|
try:
|
|
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=user_id, require_member=True)
|
|
except ListPermissionError as e:
|
|
logger.warning(f"ListPermissionError in check_list_access_for_financials for list {list_id}, user {user_id}, action '{action}': {e.detail}")
|
|
raise ListPermissionError(list_id, action=action)
|
|
except ListNotFoundError:
|
|
raise
|
|
|
|
# --- Expense Endpoints ---
|
|
@router.post(
|
|
"/expenses",
|
|
response_model=ExpensePublic,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create New Expense",
|
|
tags=["Expenses"]
|
|
)
|
|
async def create_new_expense(
|
|
expense_in: ExpenseCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
logger.info(f"User {current_user.email} creating expense: {expense_in.description}")
|
|
effective_group_id = expense_in.group_id
|
|
is_group_context = False
|
|
|
|
if expense_in.list_id:
|
|
# Check basic access to list (implies membership if list is in group)
|
|
await check_list_access_for_financials(db, expense_in.list_id, current_user.id, action="create expenses for")
|
|
list_obj = await db.get(ListModel, expense_in.list_id)
|
|
if not list_obj:
|
|
raise ListNotFoundError(expense_in.list_id)
|
|
if list_obj.group_id:
|
|
if expense_in.group_id and list_obj.group_id != expense_in.group_id:
|
|
raise InvalidOperationError(f"List {list_obj.id} belongs to group {list_obj.group_id}, not group {expense_in.group_id} specified in expense.")
|
|
effective_group_id = list_obj.group_id
|
|
is_group_context = True # Expense is tied to a group via the list
|
|
elif expense_in.group_id:
|
|
raise InvalidOperationError(f"Personal list {list_obj.id} cannot have expense associated with group {expense_in.group_id}.")
|
|
# If list is personal, no group check needed yet, handled by payer check below.
|
|
|
|
elif effective_group_id: # Only group_id provided for expense
|
|
is_group_context = True
|
|
# Ensure user is at least a member to create expense in group context
|
|
await crud_group.check_group_membership(db, group_id=effective_group_id, user_id=current_user.id, action="create expenses for")
|
|
else:
|
|
# This case should ideally be caught by earlier checks if list_id was present but list was personal.
|
|
# If somehow reached, it means no list_id and no group_id.
|
|
raise InvalidOperationError("Expense must be linked to a list_id or group_id.")
|
|
|
|
# Finalize expense payload with correct group_id if derived
|
|
expense_in_final = expense_in.model_copy(update={"group_id": effective_group_id})
|
|
|
|
# --- Granular Permission Check for Payer ---
|
|
if expense_in_final.paid_by_user_id != current_user.id:
|
|
logger.warning(f"User {current_user.email} attempting to create expense paid by other user {expense_in_final.paid_by_user_id}")
|
|
# If creating expense paid by someone else, user MUST be owner IF in group context
|
|
if is_group_context and effective_group_id:
|
|
try:
|
|
await crud_group.check_user_role_in_group(db, group_id=effective_group_id, user_id=current_user.id, required_role=UserRoleEnum.owner, action="create expense paid by another user")
|
|
except GroupPermissionError as e:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Only group owners can create expenses paid by others. {str(e)}")
|
|
else:
|
|
# Cannot create expense paid by someone else for a personal list (no group context)
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Cannot create expense paid by another user for a personal list.")
|
|
# If paying for self, basic list/group membership check above is sufficient.
|
|
|
|
try:
|
|
created_expense = await crud_expense.create_expense(db=db, expense_in=expense_in_final, current_user_id=current_user.id)
|
|
logger.info(f"Expense '{created_expense.description}' (ID: {created_expense.id}) created successfully.")
|
|
return created_expense
|
|
except (UserNotFoundError, ListNotFoundError, GroupNotFoundError, InvalidOperationError, GroupMembershipError) as e:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
except NotImplementedError as e:
|
|
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error creating expense: {str(e)}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred.")
|
|
|
|
@router.get("/expenses/{expense_id}", response_model=ExpensePublic, summary="Get Expense by ID", tags=["Expenses"])
|
|
async def get_expense(
|
|
expense_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
logger.info(f"User {current_user.email} requesting expense ID {expense_id}")
|
|
expense = await crud_expense.get_expense_by_id(db, expense_id=expense_id)
|
|
if not expense:
|
|
raise ItemNotFoundError(item_id=expense_id)
|
|
|
|
if expense.list_id:
|
|
await check_list_access_for_financials(db, expense.list_id, current_user.id)
|
|
elif expense.group_id:
|
|
await crud_group.check_group_membership(db, group_id=expense.group_id, user_id=current_user.id)
|
|
elif expense.paid_by_user_id != current_user.id:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to view this expense")
|
|
return expense
|
|
|
|
@router.get("/lists/{list_id}/expenses", response_model=PyList[ExpensePublic], summary="List Expenses for a List", tags=["Expenses", "Lists"])
|
|
async def list_list_expenses(
|
|
list_id: int,
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=200),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
logger.info(f"User {current_user.email} listing expenses for list ID {list_id}")
|
|
await check_list_access_for_financials(db, list_id, current_user.id)
|
|
expenses = await crud_expense.get_expenses_for_list(db, list_id=list_id, skip=skip, limit=limit)
|
|
return expenses
|
|
|
|
@router.get("/groups/{group_id}/expenses", response_model=PyList[ExpensePublic], summary="List Expenses for a Group", tags=["Expenses", "Groups"])
|
|
async def list_group_expenses(
|
|
group_id: int,
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=200),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
logger.info(f"User {current_user.email} listing expenses for group ID {group_id}")
|
|
await crud_group.check_group_membership(db, group_id=group_id, user_id=current_user.id, action="list expenses for")
|
|
expenses = await crud_expense.get_expenses_for_group(db, group_id=group_id, skip=skip, limit=limit)
|
|
return expenses
|
|
|
|
@router.put("/expenses/{expense_id}", response_model=ExpensePublic, summary="Update Expense", tags=["Expenses"])
|
|
async def update_expense_details(
|
|
expense_id: int,
|
|
expense_in: ExpenseUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Updates an existing expense (description, currency, expense_date only).
|
|
Requires the current version number for optimistic locking.
|
|
User must have permission to modify the expense (e.g., be the payer or group admin).
|
|
"""
|
|
logger.info(f"User {current_user.email} attempting to update expense ID {expense_id} (version {expense_in.version})")
|
|
expense_db = await crud_expense.get_expense_by_id(db, expense_id=expense_id)
|
|
if not expense_db:
|
|
raise ItemNotFoundError(item_id=expense_id)
|
|
|
|
# --- Granular Permission Check ---
|
|
can_modify = False
|
|
# 1. User paid for the expense
|
|
if expense_db.paid_by_user_id == current_user.id:
|
|
can_modify = True
|
|
# 2. OR User is owner of the group the expense belongs to
|
|
elif expense_db.group_id:
|
|
try:
|
|
await crud_group.check_user_role_in_group(db, group_id=expense_db.group_id, user_id=current_user.id, required_role=UserRoleEnum.owner, action="modify group expenses")
|
|
can_modify = True
|
|
logger.info(f"Allowing update for expense {expense_id} by group owner {current_user.email}")
|
|
except GroupMembershipError: # User not even a member
|
|
pass # Keep can_modify as False
|
|
except GroupPermissionError: # User is member but not owner
|
|
pass # Keep can_modify as False
|
|
except GroupNotFoundError: # Group doesn't exist (data integrity issue)
|
|
logger.error(f"Group {expense_db.group_id} not found for expense {expense_id} during update check.")
|
|
pass # Keep can_modify as False
|
|
# Note: If expense is only linked to a personal list (no group), only payer can modify.
|
|
|
|
if not can_modify:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User cannot modify this expense (must be payer or group owner)")
|
|
|
|
try:
|
|
updated_expense = await crud_expense.update_expense(db=db, expense_db=expense_db, expense_in=expense_in)
|
|
logger.info(f"Expense ID {expense_id} updated successfully to version {updated_expense.version}.")
|
|
return updated_expense
|
|
except InvalidOperationError as e:
|
|
# Check if it's a version conflict (409) or other validation error (400)
|
|
status_code = status.HTTP_400_BAD_REQUEST
|
|
if "version" in str(e).lower():
|
|
status_code = status.HTTP_409_CONFLICT
|
|
raise HTTPException(status_code=status_code, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error updating expense {expense_id}: {str(e)}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred.")
|
|
|
|
@router.delete("/expenses/{expense_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete Expense", tags=["Expenses"])
|
|
async def delete_expense_record(
|
|
expense_id: int,
|
|
expected_version: Optional[int] = Query(None, description="Expected version for optimistic locking"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Deletes an expense and its associated splits.
|
|
Requires expected_version query parameter for optimistic locking.
|
|
User must have permission to delete the expense (e.g., be the payer or group admin).
|
|
"""
|
|
logger.info(f"User {current_user.email} attempting to delete expense ID {expense_id} (expected version {expected_version})")
|
|
expense_db = await crud_expense.get_expense_by_id(db, expense_id=expense_id)
|
|
if not expense_db:
|
|
# Return 204 even if not found, as the end state is achieved (item is gone)
|
|
logger.warning(f"Attempt to delete non-existent expense ID {expense_id}")
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
# Alternatively, raise NotFoundError(detail=f"Expense {expense_id} not found") -> 404
|
|
|
|
# --- Granular Permission Check ---
|
|
can_delete = False
|
|
# 1. User paid for the expense
|
|
if expense_db.paid_by_user_id == current_user.id:
|
|
can_delete = True
|
|
# 2. OR User is owner of the group the expense belongs to
|
|
elif expense_db.group_id:
|
|
try:
|
|
await crud_group.check_user_role_in_group(db, group_id=expense_db.group_id, user_id=current_user.id, required_role=UserRoleEnum.owner, action="delete group expenses")
|
|
can_delete = True
|
|
logger.info(f"Allowing delete for expense {expense_id} by group owner {current_user.email}")
|
|
except GroupMembershipError:
|
|
pass
|
|
except GroupPermissionError:
|
|
pass
|
|
except GroupNotFoundError:
|
|
logger.error(f"Group {expense_db.group_id} not found for expense {expense_id} during delete check.")
|
|
pass
|
|
|
|
if not can_delete:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User cannot delete this expense (must be payer or group owner)")
|
|
|
|
try:
|
|
await crud_expense.delete_expense(db=db, expense_db=expense_db, expected_version=expected_version)
|
|
logger.info(f"Expense ID {expense_id} deleted successfully.")
|
|
# No need to return content on 204
|
|
except InvalidOperationError as e:
|
|
# Check if it's a version conflict (409) or other validation error (400)
|
|
status_code = status.HTTP_400_BAD_REQUEST
|
|
if "version" in str(e).lower():
|
|
status_code = status.HTTP_409_CONFLICT
|
|
raise HTTPException(status_code=status_code, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error deleting expense {expense_id}: {str(e)}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred.")
|
|
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
|
# --- Settlement Endpoints ---
|
|
@router.post(
|
|
"/settlements",
|
|
response_model=SettlementPublic,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Record New Settlement",
|
|
tags=["Settlements"]
|
|
)
|
|
async def create_new_settlement(
|
|
settlement_in: SettlementCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
logger.info(f"User {current_user.email} recording settlement in group {settlement_in.group_id}")
|
|
await crud_group.check_group_membership(db, group_id=settlement_in.group_id, user_id=current_user.id, action="record settlements in")
|
|
try:
|
|
await crud_group.check_group_membership(db, group_id=settlement_in.group_id, user_id=settlement_in.paid_by_user_id, action="be a payer in this group's settlement")
|
|
await crud_group.check_group_membership(db, group_id=settlement_in.group_id, user_id=settlement_in.paid_to_user_id, action="be a payee in this group's settlement")
|
|
except GroupMembershipError as e:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Payer or payee issue: {str(e)}")
|
|
except GroupNotFoundError as e:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
|
|
try:
|
|
created_settlement = await crud_settlement.create_settlement(db=db, settlement_in=settlement_in, current_user_id=current_user.id)
|
|
logger.info(f"Settlement ID {created_settlement.id} recorded successfully in group {settlement_in.group_id}.")
|
|
return created_settlement
|
|
except (UserNotFoundError, GroupNotFoundError, InvalidOperationError, GroupMembershipError) as e:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error recording settlement: {str(e)}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred.")
|
|
|
|
@router.get("/settlements/{settlement_id}", response_model=SettlementPublic, summary="Get Settlement by ID", tags=["Settlements"])
|
|
async def get_settlement(
|
|
settlement_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
logger.info(f"User {current_user.email} requesting settlement ID {settlement_id}")
|
|
settlement = await crud_settlement.get_settlement_by_id(db, settlement_id=settlement_id)
|
|
if not settlement:
|
|
raise ItemNotFoundError(item_id=settlement_id)
|
|
|
|
is_party_to_settlement = current_user.id in [settlement.paid_by_user_id, settlement.paid_to_user_id]
|
|
try:
|
|
await crud_group.check_group_membership(db, group_id=settlement.group_id, user_id=current_user.id)
|
|
except GroupMembershipError:
|
|
if not is_party_to_settlement:
|
|
raise GroupMembershipError(settlement.group_id, action="view this settlement's details")
|
|
logger.info(f"User {current_user.email} (party to settlement) viewing settlement {settlement_id} for group {settlement.group_id}.")
|
|
return settlement
|
|
|
|
@router.get("/groups/{group_id}/settlements", response_model=PyList[SettlementPublic], summary="List Settlements for a Group", tags=["Settlements", "Groups"])
|
|
async def list_group_settlements(
|
|
group_id: int,
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=200),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
logger.info(f"User {current_user.email} listing settlements for group ID {group_id}")
|
|
await crud_group.check_group_membership(db, group_id=group_id, user_id=current_user.id, action="list settlements for this group")
|
|
settlements = await crud_settlement.get_settlements_for_group(db, group_id=group_id, skip=skip, limit=limit)
|
|
return settlements
|
|
|
|
@router.put("/settlements/{settlement_id}", response_model=SettlementPublic, summary="Update Settlement", tags=["Settlements"])
|
|
async def update_settlement_details(
|
|
settlement_id: int,
|
|
settlement_in: SettlementUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Updates an existing settlement (description, settlement_date only).
|
|
Requires the current version number for optimistic locking.
|
|
User must have permission (e.g., be involved party or group admin).
|
|
"""
|
|
logger.info(f"User {current_user.email} attempting to update settlement ID {settlement_id} (version {settlement_in.version})")
|
|
settlement_db = await crud_settlement.get_settlement_by_id(db, settlement_id=settlement_id)
|
|
if not settlement_db:
|
|
raise ItemNotFoundError(item_id=settlement_id)
|
|
|
|
# --- Granular Permission Check ---
|
|
can_modify = False
|
|
# 1. User is involved party (payer or payee)
|
|
is_party = current_user.id in [settlement_db.paid_by_user_id, settlement_db.paid_to_user_id]
|
|
if is_party:
|
|
can_modify = True
|
|
# 2. OR User is owner of the group the settlement belongs to
|
|
# Note: Settlements always have a group_id based on current model
|
|
elif settlement_db.group_id:
|
|
try:
|
|
await crud_group.check_user_role_in_group(db, group_id=settlement_db.group_id, user_id=current_user.id, required_role=UserRoleEnum.owner, action="modify group settlements")
|
|
can_modify = True
|
|
logger.info(f"Allowing update for settlement {settlement_id} by group owner {current_user.email}")
|
|
except GroupMembershipError:
|
|
pass
|
|
except GroupPermissionError:
|
|
pass
|
|
except GroupNotFoundError:
|
|
logger.error(f"Group {settlement_db.group_id} not found for settlement {settlement_id} during update check.")
|
|
pass
|
|
|
|
if not can_modify:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User cannot modify this settlement (must be involved party or group owner)")
|
|
|
|
try:
|
|
updated_settlement = await crud_settlement.update_settlement(db=db, settlement_db=settlement_db, settlement_in=settlement_in)
|
|
logger.info(f"Settlement ID {settlement_id} updated successfully to version {updated_settlement.version}.")
|
|
return updated_settlement
|
|
except InvalidOperationError as e:
|
|
status_code = status.HTTP_400_BAD_REQUEST
|
|
if "version" in str(e).lower():
|
|
status_code = status.HTTP_409_CONFLICT
|
|
raise HTTPException(status_code=status_code, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error updating settlement {settlement_id}: {str(e)}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred.")
|
|
|
|
@router.delete("/settlements/{settlement_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete Settlement", tags=["Settlements"])
|
|
async def delete_settlement_record(
|
|
settlement_id: int,
|
|
expected_version: Optional[int] = Query(None, description="Expected version for optimistic locking"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Deletes a settlement.
|
|
Requires expected_version query parameter for optimistic locking.
|
|
User must have permission (e.g., be involved party or group admin).
|
|
"""
|
|
logger.info(f"User {current_user.email} attempting to delete settlement ID {settlement_id} (expected version {expected_version})")
|
|
settlement_db = await crud_settlement.get_settlement_by_id(db, settlement_id=settlement_id)
|
|
if not settlement_db:
|
|
logger.warning(f"Attempt to delete non-existent settlement ID {settlement_id}")
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
|
# --- Granular Permission Check ---
|
|
can_delete = False
|
|
# 1. User is involved party (payer or payee)
|
|
is_party = current_user.id in [settlement_db.paid_by_user_id, settlement_db.paid_to_user_id]
|
|
if is_party:
|
|
can_delete = True
|
|
# 2. OR User is owner of the group the settlement belongs to
|
|
elif settlement_db.group_id:
|
|
try:
|
|
await crud_group.check_user_role_in_group(db, group_id=settlement_db.group_id, user_id=current_user.id, required_role=UserRoleEnum.owner, action="delete group settlements")
|
|
can_delete = True
|
|
logger.info(f"Allowing delete for settlement {settlement_id} by group owner {current_user.email}")
|
|
except GroupMembershipError:
|
|
pass
|
|
except GroupPermissionError:
|
|
pass
|
|
except GroupNotFoundError:
|
|
logger.error(f"Group {settlement_db.group_id} not found for settlement {settlement_id} during delete check.")
|
|
pass
|
|
|
|
if not can_delete:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User cannot delete this settlement (must be involved party or group owner)")
|
|
|
|
try:
|
|
await crud_settlement.delete_settlement(db=db, settlement_db=settlement_db, expected_version=expected_version)
|
|
logger.info(f"Settlement ID {settlement_id} deleted successfully.")
|
|
except InvalidOperationError as e:
|
|
status_code = status.HTTP_400_BAD_REQUEST
|
|
if "version" in str(e).lower():
|
|
status_code = status.HTTP_409_CONFLICT
|
|
raise HTTPException(status_code=status_code, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error deleting settlement {settlement_id}: {str(e)}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred.")
|
|
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
|
# TODO (remaining from original list):
|
|
# (None - GET/POST/PUT/DELETE implemented for Expense/Settlement) |