mitlist/be/app/schemas/expense.py
google-labs-jules[bot] f1152c5745 feat: Implement traceable expense splitting and settlement activities
Backend:
- Added `SettlementActivity` model to track payments against specific expense shares.
- Added `status` and `paid_at` to `ExpenseSplit` model.
- Added `overall_settlement_status` to `Expense` model.
- Implemented CRUD for `SettlementActivity`, including logic to update parent expense/split statuses.
- Updated `Expense` CRUD to initialize new status fields.
- Defined Pydantic schemas for `SettlementActivity` and updated `Expense/ExpenseSplit` schemas.
- Exposed API endpoints for creating/listing settlement activities and settling shares.
- Adjusted group balance summary logic to include settlement activities.
- Added comprehensive backend unit and API tests for new functionality.

Frontend (Foundation & TODOs due to my current capabilities):
- Created TypeScript interfaces for all new/updated models.
- Set up `listDetailStore.ts` with an action to handle `settleExpenseSplit` (API call is a placeholder) and refresh data.
- Created `SettleShareModal.vue` component for payment confirmation.
- Added unit tests for the new modal and store logic.
- Updated `ListDetailPage.vue` to display detailed expense/share statuses and settlement activities.
- `mitlist_doc.md` updated to reflect all backend changes and current frontend status.
- A `TODO.md` (implicitly within `mitlist_doc.md`'s new section) outlines necessary manual frontend integrations for `api.ts` and `ListDetailPage.vue` to complete the 'Settle Share' UI flow.

This set of changes provides the core backend infrastructure for precise expense share tracking and settlement, and lays the groundwork for full frontend integration.
2025-05-22 07:05:31 +00:00

140 lines
5.5 KiB
Python

# app/schemas/expense.py
from pydantic import BaseModel, ConfigDict, validator
from typing import List, Optional
from decimal import Decimal
from datetime import datetime
# Assuming SplitTypeEnum is accessible here, e.g., from app.models or app.core.enums
# For now, let's redefine it or import it if models.py is parsable by Pydantic directly
# If it's from app.models, you might need to make app.models.SplitTypeEnum Pydantic-compatible or map it.
# For simplicity during schema definition, I'll redefine a string enum here.
# In a real setup, ensure this aligns with the SQLAlchemy enum in models.py.
from app.models import SplitTypeEnum, ExpenseSplitStatusEnum, ExpenseOverallStatusEnum # Try importing directly
from app.schemas.user import UserPublic # For user details in responses
from app.schemas.settlement_activity import SettlementActivityPublic # For settlement activities
# --- ExpenseSplit Schemas ---
class ExpenseSplitBase(BaseModel):
user_id: int
owed_amount: Decimal
share_percentage: Optional[Decimal] = None
share_units: Optional[int] = None
class ExpenseSplitCreate(ExpenseSplitBase):
pass # All fields from base are needed for creation
class ExpenseSplitPublic(ExpenseSplitBase):
id: int
expense_id: int
user: Optional[UserPublic] = None # If we want to nest user details
created_at: datetime
updated_at: datetime
status: ExpenseSplitStatusEnum # New field
paid_at: Optional[datetime] = None # New field
settlement_activities: List[SettlementActivityPublic] = [] # New field
model_config = ConfigDict(from_attributes=True)
# --- Expense Schemas ---
class ExpenseBase(BaseModel):
description: str
total_amount: Decimal
currency: Optional[str] = "USD"
expense_date: Optional[datetime] = None
split_type: SplitTypeEnum
list_id: Optional[int] = None
group_id: Optional[int] = None # Should be present if list_id is not, and vice-versa
item_id: Optional[int] = None
paid_by_user_id: int
class ExpenseCreate(ExpenseBase):
# For EQUAL split, splits are generated. For others, they might be provided.
# This logic will be in the CRUD: if split_type is EXACT_AMOUNTS, PERCENTAGE, SHARES,
# then 'splits_in' should be provided.
splits_in: Optional[List[ExpenseSplitCreate]] = None
@validator('total_amount')
def total_amount_must_be_positive(cls, v):
if v <= Decimal('0'):
raise ValueError('Total amount must be positive')
return v
# Basic validation: if list_id is None, group_id must be provided.
# More complex cross-field validation might be needed.
@validator('group_id', always=True)
def check_list_or_group_id(cls, v, values):
if values.get('list_id') is None and v is None:
raise ValueError('Either list_id or group_id must be provided for an expense')
return v
class ExpenseUpdate(BaseModel):
description: Optional[str] = None
total_amount: Optional[Decimal] = None
currency: Optional[str] = None
expense_date: Optional[datetime] = None
split_type: Optional[SplitTypeEnum] = None
list_id: Optional[int] = None
group_id: Optional[int] = None
item_id: Optional[int] = None
# paid_by_user_id is usually not updatable directly to maintain integrity.
# Updating splits would be a more complex operation, potentially a separate endpoint or careful logic.
version: int # For optimistic locking
class ExpensePublic(ExpenseBase):
id: int
created_at: datetime
updated_at: datetime
version: int
created_by_user_id: int
splits: List[ExpenseSplitPublic] = []
paid_by_user: Optional[UserPublic] = None # If nesting user details
overall_settlement_status: ExpenseOverallStatusEnum # New field
# list: Optional[ListPublic] # If nesting list details
# group: Optional[GroupPublic] # If nesting group details
# item: Optional[ItemPublic] # If nesting item details
model_config = ConfigDict(from_attributes=True)
# --- Settlement Schemas ---
class SettlementBase(BaseModel):
group_id: int
paid_by_user_id: int
paid_to_user_id: int
amount: Decimal
settlement_date: Optional[datetime] = None
description: Optional[str] = None
class SettlementCreate(SettlementBase):
@validator('amount')
def amount_must_be_positive(cls, v):
if v <= Decimal('0'):
raise ValueError('Settlement amount must be positive')
return v
@validator('paid_to_user_id')
def payer_and_payee_must_be_different(cls, v, values):
if 'paid_by_user_id' in values and v == values['paid_by_user_id']:
raise ValueError('Payer and payee cannot be the same user')
return v
class SettlementUpdate(BaseModel):
amount: Optional[Decimal] = None
settlement_date: Optional[datetime] = None
description: Optional[str] = None
# group_id, paid_by_user_id, paid_to_user_id are typically not updatable.
version: int # For optimistic locking
class SettlementPublic(SettlementBase):
id: int
created_at: datetime
updated_at: datetime
version: int
created_by_user_id: int
# payer: Optional[UserPublic] # If we want to include payer details
# payee: Optional[UserPublic] # If we want to include payee details
# group: Optional[GroupPublic] # If we want to include group details
model_config = ConfigDict(from_attributes=True)
# Placeholder for nested schemas (e.g., UserPublic) if needed
# from app.schemas.user import UserPublic
# from app.schemas.list import ListPublic
# from app.schemas.group import GroupPublic
# from app.schemas.item import ItemPublic