using BudgetApp.Data.Entities; using BudgetApp.Data.Interfaces; using Microsoft.EntityFrameworkCore; namespace BudgetApp.Data.Repositories; public class TransactionRepository(ApplicationDbContext db) : ITransactionRepository { public async Task> GetTransactionsAsync(string userId) { return await db.Transactions .Include(t => t.Account) .Include(t => t.Category) .Include(t => t.ParentTransaction) .Where(t => t.Account.UserId == userId) .OrderByDescending(t => t.Date) .ToListAsync(); } public async Task> GetRecentTransactionsAsync(string userId, int count = 5) { return await db.Transactions .Include(t => t.Account) .Include(t => t.Category) .Include(t => t.ParentTransaction) .Where(t => t.Account.UserId == userId && t.Date <= DateTime.Today) .OrderByDescending(t => t.Date) .Take(count) .ToListAsync(); } public async Task> GetUpcomingTransactionsAsync(string userId, int count = 5) { return await db.Transactions .Include(t => t.Account) .Include(t => t.Category) .Include(t => t.ParentTransaction) .Where(t => t.Account.UserId == userId && t.Date > DateTime.Today) .OrderBy(t => t.Date) .Take(count) .ToListAsync(); } public async Task> GetTransactionsInRangeAsync(string userId, DateTime from, DateTime to) { return await db.Transactions .Include(t => t.Account) .Include(t => t.Category) .Include(t => t.ParentTransaction) .Where(t => t.Account.UserId == userId && t.Date.Date >= from.Date && t.Date.Date <= to.Date) .OrderByDescending(t => t.Date) .ToListAsync(); } public async Task> GetTransactionsByAccountAsync(int accountId, string userId) { return await db.Transactions .Include(t => t.Category) .Where(t => t.AccountId == accountId && t.Account.UserId == userId) .OrderByDescending(t => t.Date) .ToListAsync(); } public async Task CreateTransactionAsync(Transaction transaction) { // For recurring transactions: enforce max 1 year end date if (transaction.RecurrenceType != RecurrenceType.None) { var maxEnd = transaction.Date.AddYears(1); if (transaction.RecurrenceEndDate is null || transaction.RecurrenceEndDate.Value > maxEnd) { transaction.RecurrenceEndDate = maxEnd; } } db.Transactions.Add(transaction); await db.SaveChangesAsync(); // Immediately materialize child occurrences for recurring transactions if (transaction.RecurrenceType != RecurrenceType.None) { var endDate = transaction.RecurrenceEndDate ?? transaction.Date.AddYears(1); var newTransactions = new List(); var nextDate = GetNextOccurrence(transaction.Date, transaction.RecurrenceType); while (nextDate <= endDate) { newTransactions.Add(new Transaction { Description = transaction.Description, Amount = transaction.Amount, Date = nextDate, Type = transaction.Type, AccountId = transaction.AccountId, CategoryId = transaction.CategoryId, RecurrenceType = RecurrenceType.None, ParentTransactionId = transaction.Id }); nextDate = GetNextOccurrence(nextDate, transaction.RecurrenceType); } if (newTransactions.Count > 0) { db.Transactions.AddRange(newTransactions); await db.SaveChangesAsync(); } } } public async Task UpdateTransactionAsync(int id, string userId, string description, decimal amount, DateTime date, TransactionType type, int accountId, int? categoryId, RecurrenceType recurrenceType = RecurrenceType.None, DateTime? recurrenceEndDate = null) { var transaction = await db.Transactions .Include(t => t.Account) .FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId); if (transaction is null) return; var isPartOfSeries = transaction.RecurrenceType != RecurrenceType.None || transaction.ParentTransactionId is not null; transaction.Description = description; transaction.Amount = amount; transaction.Date = date; transaction.Type = type; transaction.AccountId = accountId; transaction.CategoryId = categoryId; if (isPartOfSeries) { var parentId = transaction.ParentTransactionId ?? transaction.Id; var cutoffDate = transaction.Date.Date; var futureSiblings = await db.Transactions .Where(t => (t.Id == parentId || t.ParentTransactionId == parentId) && t.Id != id && t.Date.Date >= cutoffDate) .ToListAsync(); foreach (var sibling in futureSiblings) { sibling.Description = description; sibling.Amount = amount; sibling.Type = type; sibling.AccountId = accountId; sibling.CategoryId = categoryId; } } else { transaction.RecurrenceType = recurrenceType; transaction.RecurrenceEndDate = recurrenceEndDate; } await db.SaveChangesAsync(); } public async Task DeleteTransactionAsync(int id, string userId) { var transaction = await db.Transactions .FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId); if (transaction is not null) { db.Transactions.Remove(transaction); await db.SaveChangesAsync(); } } public async Task DeleteTransactionSeriesAsync(int id, string userId) { var transaction = await db.Transactions .Include(t => t.Account) .FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId); if (transaction is null) return; var parentId = transaction.ParentTransactionId ?? transaction.Id; var cutoffDate = transaction.Date.Date; var allChildren = await db.Transactions .Where(t => t.ParentTransactionId == parentId) .ToListAsync(); var toDelete = allChildren.Where(c => c.Date.Date >= cutoffDate).ToList(); var toKeep = allChildren.Where(c => c.Date.Date < cutoffDate).ToList(); db.Transactions.RemoveRange(toDelete); var parent = parentId == transaction.Id ? transaction : await db.Transactions .FirstOrDefaultAsync(t => t.Id == parentId && t.Account.UserId == userId); if (parent is not null) { if (parent.Date.Date >= cutoffDate) { foreach (var child in toKeep) { child.ParentTransactionId = null; } db.Transactions.Remove(parent); } else { parent.RecurrenceEndDate = cutoffDate.AddDays(-1); } } await db.SaveChangesAsync(); } public async Task MarkAsPaidAsync(int id, string userId, bool isPaid = true) { var transaction = await db.Transactions .Include(t => t.Account) .FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId); if (transaction is null) return; transaction.IsPaid = isPaid; await db.SaveChangesAsync(); } public async Task MaterializeRecurringTransactionsAsync(string userId, DateTime? windowEnd = null) { var recurringParents = await db.Transactions .Include(t => t.Account) .Where(t => t.Account.UserId == userId && t.RecurrenceType != RecurrenceType.None && t.ParentTransactionId == null) .ToListAsync(); var defaultHorizon = DateTime.Today.AddDays(60); var horizon = windowEnd.HasValue && windowEnd.Value > defaultHorizon ? windowEnd.Value : defaultHorizon; var newTransactions = new List(); foreach (var parent in recurringParents) { var endDate = parent.RecurrenceEndDate.HasValue && parent.RecurrenceEndDate.Value < horizon ? parent.RecurrenceEndDate.Value : horizon; var latestChildDate = await db.Transactions .Where(t => t.ParentTransactionId == parent.Id) .OrderByDescending(t => t.Date) .Select(t => (DateTime?)t.Date) .FirstOrDefaultAsync(); var lastDate = latestChildDate ?? parent.Date; var nextDate = GetNextOccurrence(lastDate, parent.RecurrenceType); while (nextDate <= endDate) { newTransactions.Add(new Transaction { Description = parent.Description, Amount = parent.Amount, Date = nextDate, Type = parent.Type, AccountId = parent.AccountId, CategoryId = parent.CategoryId, RecurrenceType = RecurrenceType.None, ParentTransactionId = parent.Id }); nextDate = GetNextOccurrence(nextDate, parent.RecurrenceType); } } if (newTransactions.Count > 0) { db.Transactions.AddRange(newTransactions); await db.SaveChangesAsync(); } } internal static DateTime GetNextOccurrence(DateTime current, RecurrenceType recurrence) => recurrence switch { RecurrenceType.Daily => current.AddDays(1), RecurrenceType.Weekly => current.AddDays(7), RecurrenceType.FourWeekly => current.AddDays(28), RecurrenceType.Monthly => current.AddMonths(1), RecurrenceType.Yearly => current.AddYears(1), _ => DateTime.MaxValue }; }