from datetime import datetime, timedelta from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, and_ from app.models import Expense, RecurrencePattern from app.crud.expense import create_expense from app.schemas.expense import ExpenseCreate import logging from typing import Optional logger = logging.getLogger(__name__) async def generate_recurring_expenses(db: AsyncSession) -> None: """ Background job to generate recurring expenses. Should be run daily to check for and create new recurring expenses. """ try: # Get all active recurring expenses that need to be generated now = datetime.utcnow() query = select(Expense).join(RecurrencePattern).where( and_( Expense.is_recurring == True, Expense.next_occurrence <= now, # Check if we haven't reached max occurrences ( (RecurrencePattern.max_occurrences == None) | (RecurrencePattern.max_occurrences > 0) ), # Check if we haven't reached end date ( (RecurrencePattern.end_date == None) | (RecurrencePattern.end_date > now) ) ) ) result = await db.execute(query) recurring_expenses = result.scalars().all() for expense in recurring_expenses: try: await _generate_next_occurrence(db, expense) except Exception as e: logger.error(f"Error generating next occurrence for expense {expense.id}: {str(e)}") continue except Exception as e: logger.error(f"Error in generate_recurring_expenses job: {str(e)}") raise async def _generate_next_occurrence(db: AsyncSession, expense: Expense) -> None: """Generate the next occurrence of a recurring expense.""" pattern = expense.recurrence_pattern if not pattern: return # Calculate next occurrence date next_date = _calculate_next_occurrence(expense.next_occurrence, pattern) if not next_date: return # Create new expense based on template new_expense = ExpenseCreate( description=expense.description, total_amount=expense.total_amount, currency=expense.currency, expense_date=next_date, split_type=expense.split_type, list_id=expense.list_id, group_id=expense.group_id, item_id=expense.item_id, paid_by_user_id=expense.paid_by_user_id, is_recurring=False, # Generated expenses are not recurring splits_in=None # Will be generated based on split_type ) # Create the new expense created_expense = await create_expense(db, new_expense, expense.created_by_user_id) # Update the original expense expense.last_occurrence = next_date expense.next_occurrence = _calculate_next_occurrence(next_date, pattern) if pattern.max_occurrences: pattern.max_occurrences -= 1 await db.flush() def _calculate_next_occurrence(current_date: datetime, pattern: RecurrencePattern) -> Optional[datetime]: """Calculate the next occurrence date based on the pattern.""" if not current_date: return None if pattern.type == 'daily': return current_date + timedelta(days=pattern.interval) elif pattern.type == 'weekly': if not pattern.days_of_week: return current_date + timedelta(weeks=pattern.interval) # Find next day of week current_weekday = current_date.weekday() next_weekday = min((d for d in pattern.days_of_week if d > current_weekday), default=min(pattern.days_of_week)) days_ahead = next_weekday - current_weekday if days_ahead <= 0: days_ahead += 7 return current_date + timedelta(days=days_ahead) elif pattern.type == 'monthly': # Add months to current date year = current_date.year + (current_date.month + pattern.interval - 1) // 12 month = (current_date.month + pattern.interval - 1) % 12 + 1 return current_date.replace(year=year, month=month) elif pattern.type == 'yearly': return current_date.replace(year=current_date.year + pattern.interval) return None