""" Recurring Service — processes recurring transaction rules and creates due transactions. Called by cron daily at 6AM. """ import logging from datetime import date, timedelta from dateutil.relativedelta import relativedelta from app.extensions import db from app.models.recurring_rule import RecurringRule from app.models.transaction import Transaction from app.services.account_service import calc_balance log = logging.getLogger(__name__) def next_occurrence(last_run, frequency): """Calculate the next due date given the last run date and frequency.""" if frequency == 'daily': return last_run + timedelta(days=1) elif frequency == 'weekly': return last_run + timedelta(weeks=1) elif frequency == 'biweekly': return last_run + timedelta(weeks=2) elif frequency == 'monthly': return last_run + relativedelta(months=1) elif frequency == 'quarterly': return last_run + relativedelta(months=3) elif frequency == 'yearly': return last_run + relativedelta(years=1) return last_run + relativedelta(months=1) def process_due_rules(dry_run=False): """ Find all active recurring rules that are due today or overdue. Create transactions for each due occurrence. Returns list of created transaction descriptions. """ today = date.today() created = [] rules = RecurringRule.query.filter( RecurringRule.is_active == True, RecurringRule.next_run <= today, ).all() for rule in rules: # Check end date if rule.end_date and today > rule.end_date: rule.is_active = False if not dry_run: db.session.commit() continue # Create transaction for each missed occurrence up to today run_date = rule.next_run or rule.start_date affected_accounts = set() while run_date <= today: if not dry_run: txn = Transaction( transaction_type=rule.transaction_type, account_id=rule.account_id, category_id=rule.category_id, amount=rule.amount, description=rule.description, date=run_date, is_recurring=True, recurring_rule_id=rule.id, ) db.session.add(txn) affected_accounts.add(rule.account_id) created.append(f'{rule.description} ({rule.transaction_type}) on {run_date}') log.info(f'[recurring] {"DRY " if dry_run else ""}Created: {rule.description} on {run_date}') rule.last_run = run_date run_date = next_occurrence(run_date, rule.frequency) rule.next_run = run_date if not dry_run: db.session.commit() for account_id in affected_accounts: calc_balance(account_id) return created def get_upcoming(days=30): """Return list of upcoming recurring transactions in the next N days.""" today = date.today() cutoff = today + timedelta(days=days) rules = RecurringRule.query.filter( RecurringRule.is_active == True, ).all() upcoming = [] for rule in rules: next_date = rule.next_run or rule.start_date while next_date <= cutoff: if next_date >= today: upcoming.append({ 'rule': rule, 'date': next_date, 'description': rule.description, 'amount': float(rule.amount), 'type': rule.transaction_type, }) next_date = next_occurrence(next_date, rule.frequency) upcoming.sort(key=lambda x: x['date']) return upcoming