
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.
152 lines
5.5 KiB
Python
152 lines
5.5 KiB
Python
from pydantic import BaseModel, ConfigDict, validator, Field
|
|
from typing import List, Optional
|
|
from decimal import Decimal
|
|
from datetime import datetime
|
|
from app.models import SplitTypeEnum, ExpenseSplitStatusEnum, ExpenseOverallStatusEnum
|
|
from app.schemas.user import UserPublic
|
|
from app.schemas.settlement_activity import SettlementActivityPublic
|
|
from app.schemas.recurrence import RecurrencePatternCreate, RecurrencePatternPublic
|
|
|
|
class ExpenseSplitBase(BaseModel):
|
|
user_id: int
|
|
owed_amount: Optional[Decimal] = None
|
|
share_percentage: Optional[Decimal] = None
|
|
share_units: Optional[int] = None
|
|
# Note: Status is handled by the backend, not in create/update payloads
|
|
|
|
class ExpenseSplitCreate(ExpenseSplitBase):
|
|
pass
|
|
|
|
class ExpenseSplitPublic(ExpenseSplitBase):
|
|
id: int
|
|
expense_id: int
|
|
status: ExpenseSplitStatusEnum
|
|
user: Optional[UserPublic] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
paid_at: Optional[datetime] = None
|
|
settlement_activities: List[SettlementActivityPublic] = []
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
class RecurrencePatternBase(BaseModel):
|
|
type: str = Field(..., description="Type of recurrence: daily, weekly, monthly, yearly")
|
|
interval: int = Field(..., description="Interval of recurrence (e.g., every X days/weeks/months/years)")
|
|
days_of_week: Optional[List[int]] = Field(None, description="Days of week for weekly recurrence (0-6, Sunday-Saturday)")
|
|
end_date: Optional[datetime] = Field(None, description="Optional end date for the recurrence")
|
|
max_occurrences: Optional[int] = Field(None, description="Optional maximum number of occurrences")
|
|
|
|
class RecurrencePatternCreate(RecurrencePatternBase):
|
|
pass
|
|
|
|
class RecurrencePatternUpdate(RecurrencePatternBase):
|
|
pass
|
|
|
|
class RecurrencePatternInDB(RecurrencePatternBase):
|
|
id: int
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
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
|
|
item_id: Optional[int] = None
|
|
paid_by_user_id: int
|
|
is_recurring: bool = Field(False, description="Whether this is a recurring expense")
|
|
recurrence_pattern: Optional[RecurrencePatternCreate] = Field(None, description="Recurrence pattern for recurring expenses")
|
|
|
|
class ExpenseCreate(ExpenseBase):
|
|
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
|
|
|
|
@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
|
|
|
|
@validator('recurrence_pattern')
|
|
def validate_recurrence_pattern(cls, v, values):
|
|
if values.get('is_recurring') and not v:
|
|
raise ValueError('Recurrence pattern is required for recurring expenses')
|
|
if not values.get('is_recurring') and v:
|
|
raise ValueError('Recurrence pattern should not be provided for non-recurring expenses')
|
|
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
|
|
version: int
|
|
is_recurring: Optional[bool] = None
|
|
recurrence_pattern: Optional[RecurrencePatternUpdate] = None
|
|
next_occurrence: Optional[datetime] = None
|
|
|
|
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
|
|
overall_settlement_status: ExpenseOverallStatusEnum
|
|
is_recurring: bool
|
|
next_occurrence: Optional[datetime]
|
|
last_occurrence: Optional[datetime]
|
|
recurrence_pattern: Optional[RecurrencePatternInDB]
|
|
parent_expense_id: Optional[int]
|
|
generated_expenses: List['ExpensePublic'] = []
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
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
|
|
version: int
|
|
|
|
class SettlementPublic(SettlementBase):
|
|
id: int
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
version: int
|
|
created_by_user_id: int
|
|
model_config = ConfigDict(from_attributes=True) |