using BudgetApp.Data; using BudgetApp.Services; using BudgetApp.Tests.Helpers; using Microsoft.EntityFrameworkCore; using Xunit; namespace BudgetApp.Tests.Services; public class TransactionServiceTests { private static Account SeedAccount(ApplicationDbContext ctx, string userId = TestDbContextFactory.TestUserId) { var account = new Account { Name = "Test Account", UserId = userId }; ctx.Accounts.Add(account); ctx.SaveChanges(); return account; } #region GetTransactionsAsync [Fact] public async Task GetTransactionsAsync_ReturnsUserTransactions_OrderedByDateDescending() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); ctx.Transactions.AddRange( new Transaction { Description = "Old", Amount = 10, Date = new DateTime(2025, 1, 1), AccountId = account.Id }, new Transaction { Description = "New", Amount = 20, Date = new DateTime(2025, 6, 1), AccountId = account.Id }); ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId); Assert.Equal(2, result.Count); Assert.Equal("New", result[0].Description); Assert.Equal("Old", result[1].Description); } [Fact] public async Task GetTransactionsAsync_ExcludesOtherUserTransactions() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { var mine = SeedAccount(ctx, TestDbContextFactory.TestUserId); var theirs = SeedAccount(ctx, TestDbContextFactory.OtherUserId); ctx.Transactions.AddRange( new Transaction { Description = "Mine", Amount = 10, Date = DateTime.Today, AccountId = mine.Id }, new Transaction { Description = "Theirs", Amount = 20, Date = DateTime.Today, AccountId = theirs.Id }); ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId); Assert.Single(result); Assert.Equal("Mine", result[0].Description); } [Fact] public async Task GetTransactionsAsync_IncludesRelatedEntities() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); var category = new Category { Name = "Food", UserId = TestDbContextFactory.TestUserId }; ctx.Categories.Add(category); ctx.SaveChanges(); ctx.Transactions.Add(new Transaction { Description = "Lunch", Amount = 10, Date = DateTime.Today, AccountId = account.Id, CategoryId = category.Id }); ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId); Assert.Single(result); Assert.NotNull(result[0].Account); Assert.NotNull(result[0].Category); Assert.Equal("Food", result[0].Category!.Name); } #endregion #region GetRecentTransactionsAsync [Fact] public async Task GetRecentTransactionsAsync_ReturnsTransactionsOnOrBeforeToday() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); ctx.Transactions.AddRange( new Transaction { Description = "Past", Amount = 10, Date = DateTime.Today.AddDays(-1), AccountId = account.Id }, new Transaction { Description = "Today", Amount = 20, Date = DateTime.Today, AccountId = account.Id }, new Transaction { Description = "Future", Amount = 30, Date = DateTime.Today.AddDays(1), AccountId = account.Id }); ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); var result = await service.GetRecentTransactionsAsync(TestDbContextFactory.TestUserId, 10); Assert.Equal(2, result.Count); Assert.DoesNotContain(result, t => t.Description == "Future"); } [Fact] public async Task GetRecentTransactionsAsync_RespectsCount() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); for (int i = 0; i < 10; i++) { ctx.Transactions.Add(new Transaction { Description = $"T{i}", Amount = i, Date = DateTime.Today.AddDays(-i), AccountId = account.Id }); } ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); var result = await service.GetRecentTransactionsAsync(TestDbContextFactory.TestUserId, 3); Assert.Equal(3, result.Count); } #endregion #region GetUpcomingTransactionsAsync [Fact] public async Task GetUpcomingTransactionsAsync_ReturnsTransactionsAfterToday() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); ctx.Transactions.AddRange( new Transaction { Description = "Past", Amount = 10, Date = DateTime.Today.AddDays(-1), AccountId = account.Id }, new Transaction { Description = "Today", Amount = 20, Date = DateTime.Today, AccountId = account.Id }, new Transaction { Description = "Future", Amount = 30, Date = DateTime.Today.AddDays(1), AccountId = account.Id }); ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); var result = await service.GetUpcomingTransactionsAsync(TestDbContextFactory.TestUserId, 10); Assert.Single(result); Assert.Equal("Future", result[0].Description); } #endregion #region GetTransactionsInRangeAsync [Fact] public async Task GetTransactionsInRangeAsync_ReturnsTransactionsInRange() { using var factory = new TestDbContextFactory(); var from = new DateTime(2025, 3, 1); var to = new DateTime(2025, 3, 31); using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); ctx.Transactions.AddRange( new Transaction { Description = "Before", Amount = 10, Date = new DateTime(2025, 2, 28), AccountId = account.Id }, new Transaction { Description = "Start", Amount = 20, Date = from, AccountId = account.Id }, new Transaction { Description = "Middle", Amount = 30, Date = new DateTime(2025, 3, 15), AccountId = account.Id }, new Transaction { Description = "End", Amount = 40, Date = to, AccountId = account.Id }, new Transaction { Description = "After", Amount = 50, Date = new DateTime(2025, 4, 1), AccountId = account.Id }); ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); var result = await service.GetTransactionsInRangeAsync(TestDbContextFactory.TestUserId, from, to); Assert.Equal(3, result.Count); Assert.DoesNotContain(result, t => t.Description == "Before"); Assert.DoesNotContain(result, t => t.Description == "After"); } #endregion #region GetTransactionsByAccountAsync [Fact] public async Task GetTransactionsByAccountAsync_ReturnsAccountTransactions() { using var factory = new TestDbContextFactory(); int accountId; using (var ctx = factory.CreateContext()) { var account1 = SeedAccount(ctx); var account2 = new Account { Name = "Other", UserId = TestDbContextFactory.TestUserId }; ctx.Accounts.Add(account2); ctx.SaveChanges(); accountId = account1.Id; ctx.Transactions.AddRange( new Transaction { Description = "A1", Amount = 10, Date = DateTime.Today, AccountId = account1.Id }, new Transaction { Description = "A2", Amount = 20, Date = DateTime.Today, AccountId = account2.Id }); ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); var result = await service.GetTransactionsByAccountAsync(accountId, TestDbContextFactory.TestUserId); Assert.Single(result); Assert.Equal("A1", result[0].Description); } [Fact] public async Task GetTransactionsByAccountAsync_WrongUser_ReturnsEmpty() { using var factory = new TestDbContextFactory(); int accountId; using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx, TestDbContextFactory.TestUserId); accountId = account.Id; ctx.Transactions.Add(new Transaction { Description = "T", Amount = 10, Date = DateTime.Today, AccountId = account.Id }); ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); var result = await service.GetTransactionsByAccountAsync(accountId, TestDbContextFactory.OtherUserId); Assert.Empty(result); } #endregion #region CreateTransactionAsync [Fact] public async Task CreateTransactionAsync_AddsTransaction() { using var factory = new TestDbContextFactory(); int accountId; using (var ctx = factory.CreateContext()) { accountId = SeedAccount(ctx).Id; } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.CreateTransactionAsync(new Transaction { Description = "Groceries", Amount = 50, Date = DateTime.Today, Type = TransactionType.Expense, AccountId = accountId }); using var verifyCtx = factory.CreateContext(); Assert.Single(verifyCtx.Transactions); } [Fact] public async Task CreateTransactionAsync_NonRecurring_DoesNotCreateChildren() { using var factory = new TestDbContextFactory(); int accountId; using (var ctx = factory.CreateContext()) { accountId = SeedAccount(ctx).Id; } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.CreateTransactionAsync(new Transaction { Description = "One-off", Amount = 10, Date = DateTime.Today, RecurrenceType = RecurrenceType.None, AccountId = accountId }); using var verifyCtx = factory.CreateContext(); Assert.Single(verifyCtx.Transactions); Assert.Null(verifyCtx.Transactions.Single().ParentTransactionId); } [Fact] public async Task CreateTransactionAsync_Recurring_CapsEndDateToOneYear() { using var factory = new TestDbContextFactory(); int accountId; using (var ctx = factory.CreateContext()) { accountId = SeedAccount(ctx).Id; } var startDate = new DateTime(2025, 1, 1); using var context = factory.CreateContext(); var service = new TransactionService(context); await service.CreateTransactionAsync(new Transaction { Description = "Recurring", Amount = 100, Date = startDate, RecurrenceType = RecurrenceType.Monthly, RecurrenceEndDate = startDate.AddYears(5), // way beyond 1 year AccountId = accountId }); using var verifyCtx = factory.CreateContext(); var parent = await verifyCtx.Transactions .FirstAsync(t => t.ParentTransactionId == null); Assert.Equal(startDate.AddYears(1), parent.RecurrenceEndDate); } [Fact] public async Task CreateTransactionAsync_Recurring_DefaultsEndDateWhenNull() { using var factory = new TestDbContextFactory(); int accountId; using (var ctx = factory.CreateContext()) { accountId = SeedAccount(ctx).Id; } var startDate = new DateTime(2025, 1, 1); using var context = factory.CreateContext(); var service = new TransactionService(context); await service.CreateTransactionAsync(new Transaction { Description = "Recurring", Amount = 100, Date = startDate, RecurrenceType = RecurrenceType.Monthly, RecurrenceEndDate = null, AccountId = accountId }); using var verifyCtx = factory.CreateContext(); var parent = await verifyCtx.Transactions .FirstAsync(t => t.ParentTransactionId == null); Assert.Equal(startDate.AddYears(1), parent.RecurrenceEndDate); } [Fact] public async Task CreateTransactionAsync_Recurring_MaterializesChildren() { using var factory = new TestDbContextFactory(); int accountId; using (var ctx = factory.CreateContext()) { accountId = SeedAccount(ctx).Id; } var startDate = new DateTime(2025, 1, 1); using var context = factory.CreateContext(); var service = new TransactionService(context); await service.CreateTransactionAsync(new Transaction { Description = "Weekly", Amount = 50, Date = startDate, RecurrenceType = RecurrenceType.Weekly, RecurrenceEndDate = startDate.AddDays(21), // 3 weeks = 3 children AccountId = accountId }); using var verifyCtx = factory.CreateContext(); var all = await verifyCtx.Transactions.ToListAsync(); var parent = all.Single(t => t.ParentTransactionId == null); var children = all.Where(t => t.ParentTransactionId == parent.Id).ToList(); Assert.Equal(3, children.Count); Assert.All(children, c => { Assert.Equal("Weekly", c.Description); Assert.Equal(50m, c.Amount); Assert.Equal(RecurrenceType.None, c.RecurrenceType); Assert.Equal(parent.Id, c.ParentTransactionId); }); } [Fact] public async Task CreateTransactionAsync_Recurring_Monthly_MaterializesCorrectDates() { using var factory = new TestDbContextFactory(); int accountId; using (var ctx = factory.CreateContext()) { accountId = SeedAccount(ctx).Id; } var startDate = new DateTime(2025, 1, 15); using var context = factory.CreateContext(); var service = new TransactionService(context); await service.CreateTransactionAsync(new Transaction { Description = "Monthly", Amount = 100, Date = startDate, RecurrenceType = RecurrenceType.Monthly, RecurrenceEndDate = new DateTime(2025, 4, 15), // 3 months → 3 children AccountId = accountId }); using var verifyCtx = factory.CreateContext(); var children = await verifyCtx.Transactions .Where(t => t.ParentTransactionId != null) .OrderBy(t => t.Date) .ToListAsync(); Assert.Equal(3, children.Count); Assert.Equal(new DateTime(2025, 2, 15), children[0].Date); Assert.Equal(new DateTime(2025, 3, 15), children[1].Date); Assert.Equal(new DateTime(2025, 4, 15), children[2].Date); } #endregion #region UpdateTransactionAsync [Fact] public async Task UpdateTransactionAsync_UpdatesAllFields() { using var factory = new TestDbContextFactory(); int transactionId, accountId, newAccountId; int categoryId; using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); accountId = account.Id; var newAccount = new Account { Name = "Other", UserId = TestDbContextFactory.TestUserId }; ctx.Accounts.Add(newAccount); var category = new Category { Name = "Food", UserId = TestDbContextFactory.TestUserId }; ctx.Categories.Add(category); ctx.SaveChanges(); newAccountId = newAccount.Id; categoryId = category.Id; var transaction = new Transaction { Description = "Old", Amount = 10, Date = DateTime.Today, Type = TransactionType.Expense, AccountId = accountId }; ctx.Transactions.Add(transaction); ctx.SaveChanges(); transactionId = transaction.Id; } var newDate = DateTime.Today.AddDays(5); using var context = factory.CreateContext(); var service = new TransactionService(context); await service.UpdateTransactionAsync( transactionId, TestDbContextFactory.TestUserId, "Updated", 99m, newDate, TransactionType.Income, newAccountId, categoryId, RecurrenceType.Weekly, newDate.AddMonths(3)); using var verifyCtx = factory.CreateContext(); var updated = verifyCtx.Transactions.Single(); Assert.Equal("Updated", updated.Description); Assert.Equal(99m, updated.Amount); Assert.Equal(newDate, updated.Date); Assert.Equal(TransactionType.Income, updated.Type); Assert.Equal(newAccountId, updated.AccountId); Assert.Equal(categoryId, updated.CategoryId); Assert.Equal(RecurrenceType.Weekly, updated.RecurrenceType); Assert.Equal(newDate.AddMonths(3), updated.RecurrenceEndDate); } [Fact] public async Task UpdateTransactionAsync_WrongUser_DoesNothing() { using var factory = new TestDbContextFactory(); int transactionId; using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); var transaction = new Transaction { Description = "Original", Amount = 10, Date = DateTime.Today, AccountId = account.Id }; ctx.Transactions.Add(transaction); ctx.SaveChanges(); transactionId = transaction.Id; } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.UpdateTransactionAsync( transactionId, TestDbContextFactory.OtherUserId, "Hacked", 999m, DateTime.Today, TransactionType.Income, 1, null); using var verifyCtx = factory.CreateContext(); Assert.Equal("Original", verifyCtx.Transactions.Single().Description); } #endregion #region DeleteTransactionAsync [Fact] public async Task DeleteTransactionAsync_RemovesTransaction() { using var factory = new TestDbContextFactory(); int transactionId; using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); var transaction = new Transaction { Description = "ToDelete", Amount = 10, Date = DateTime.Today, AccountId = account.Id }; ctx.Transactions.Add(transaction); ctx.SaveChanges(); transactionId = transaction.Id; } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.DeleteTransactionAsync(transactionId, TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); Assert.Empty(verifyCtx.Transactions); } [Fact] public async Task DeleteTransactionAsync_WrongUser_DoesNothing() { using var factory = new TestDbContextFactory(); int transactionId; using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); var transaction = new Transaction { Description = "Protected", Amount = 10, Date = DateTime.Today, AccountId = account.Id }; ctx.Transactions.Add(transaction); ctx.SaveChanges(); transactionId = transaction.Id; } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.DeleteTransactionAsync(transactionId, TestDbContextFactory.OtherUserId); using var verifyCtx = factory.CreateContext(); Assert.Single(verifyCtx.Transactions); } #endregion #region DeleteTransactionSeriesAsync [Fact] public async Task DeleteTransactionSeriesAsync_ChildClick_DeletesFromCutoffForward() { using var factory = new TestDbContextFactory(); int childId; using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); var parent = new Transaction { Description = "Recurring", Amount = 100, Date = new DateTime(2025, 1, 1), RecurrenceType = RecurrenceType.Monthly, RecurrenceEndDate = new DateTime(2025, 6, 1), AccountId = account.Id }; ctx.Transactions.Add(parent); ctx.SaveChanges(); // Add children: Feb, Mar, Apr, May, Jun var children = new List(); for (int m = 2; m <= 6; m++) { children.Add(new Transaction { Description = "Recurring", Amount = 100, Date = new DateTime(2025, m, 1), RecurrenceType = RecurrenceType.None, ParentTransactionId = parent.Id, AccountId = account.Id }); } ctx.Transactions.AddRange(children); ctx.SaveChanges(); // Click on the April child (cutoff = Apr 1) childId = children[2].Id; // Apr } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.DeleteTransactionSeriesAsync(childId, TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); var remaining = await verifyCtx.Transactions.OrderBy(t => t.Date).ToListAsync(); // Parent (Jan) + Feb + Mar should remain; Apr, May, Jun deleted Assert.Equal(3, remaining.Count); Assert.Contains(remaining, t => t.Date.Month == 1 && t.ParentTransactionId == null); // parent Assert.Contains(remaining, t => t.Date.Month == 2); Assert.Contains(remaining, t => t.Date.Month == 3); } [Fact] public async Task DeleteTransactionSeriesAsync_ChildClick_CapsParentRecurrence() { using var factory = new TestDbContextFactory(); int childId, parentId; using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); var parent = new Transaction { Description = "Recurring", Amount = 100, Date = new DateTime(2025, 1, 1), RecurrenceType = RecurrenceType.Monthly, RecurrenceEndDate = new DateTime(2025, 12, 1), AccountId = account.Id }; ctx.Transactions.Add(parent); ctx.SaveChanges(); parentId = parent.Id; var child = new Transaction { Description = "Recurring", Amount = 100, Date = new DateTime(2025, 4, 1), RecurrenceType = RecurrenceType.None, ParentTransactionId = parent.Id, AccountId = account.Id }; ctx.Transactions.Add(child); ctx.SaveChanges(); childId = child.Id; } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.DeleteTransactionSeriesAsync(childId, TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); var updatedParent = await verifyCtx.Transactions.FindAsync(parentId); Assert.NotNull(updatedParent); Assert.Equal(new DateTime(2025, 3, 31), updatedParent.RecurrenceEndDate); // cutoff - 1 day } [Fact] public async Task DeleteTransactionSeriesAsync_ParentClick_DeletesParentAndAllChildren() { using var factory = new TestDbContextFactory(); int parentId; using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); var parent = new Transaction { Description = "Recurring", Amount = 100, Date = new DateTime(2025, 1, 1), RecurrenceType = RecurrenceType.Monthly, RecurrenceEndDate = new DateTime(2025, 3, 1), AccountId = account.Id }; ctx.Transactions.Add(parent); ctx.SaveChanges(); parentId = parent.Id; ctx.Transactions.AddRange( new Transaction { Description = "Recurring", Amount = 100, Date = new DateTime(2025, 2, 1), RecurrenceType = RecurrenceType.None, ParentTransactionId = parent.Id, AccountId = account.Id }, new Transaction { Description = "Recurring", Amount = 100, Date = new DateTime(2025, 3, 1), RecurrenceType = RecurrenceType.None, ParentTransactionId = parent.Id, AccountId = account.Id }); ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.DeleteTransactionSeriesAsync(parentId, TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); Assert.Empty(verifyCtx.Transactions); } [Fact] public async Task DeleteTransactionSeriesAsync_WrongUser_DoesNothing() { using var factory = new TestDbContextFactory(); int transactionId; using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); var transaction = new Transaction { Description = "Protected", Amount = 10, Date = DateTime.Today, AccountId = account.Id }; ctx.Transactions.Add(transaction); ctx.SaveChanges(); transactionId = transaction.Id; } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.DeleteTransactionSeriesAsync(transactionId, TestDbContextFactory.OtherUserId); using var verifyCtx = factory.CreateContext(); Assert.Single(verifyCtx.Transactions); } #endregion #region MaterializeRecurringTransactionsAsync [Fact] public async Task MaterializeRecurringTransactionsAsync_FillsGaps() { using var factory = new TestDbContextFactory(); int parentId; var startDate = DateTime.Today; using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); var parent = new Transaction { Description = "Daily", Amount = 5, Date = startDate, RecurrenceType = RecurrenceType.Daily, RecurrenceEndDate = startDate.AddDays(10), AccountId = account.Id }; ctx.Transactions.Add(parent); ctx.SaveChanges(); parentId = parent.Id; // Only 2 children exist (days 1 and 2) ctx.Transactions.AddRange( new Transaction { Description = "Daily", Amount = 5, Date = startDate.AddDays(1), RecurrenceType = RecurrenceType.None, ParentTransactionId = parent.Id, AccountId = account.Id }, new Transaction { Description = "Daily", Amount = 5, Date = startDate.AddDays(2), RecurrenceType = RecurrenceType.None, ParentTransactionId = parent.Id, AccountId = account.Id }); ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId, startDate.AddDays(100)); using var verifyCtx = factory.CreateContext(); var children = await verifyCtx.Transactions .Where(t => t.ParentTransactionId == parentId) .ToListAsync(); // Days 1-10 should be filled = 10 children Assert.Equal(10, children.Count); } [Fact] public async Task MaterializeRecurringTransactionsAsync_RespectsRecurrenceEndDate() { using var factory = new TestDbContextFactory(); int parentId; var startDate = DateTime.Today; using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); var parent = new Transaction { Description = "Weekly", Amount = 10, Date = startDate, RecurrenceType = RecurrenceType.Weekly, RecurrenceEndDate = startDate.AddDays(14), // 2 weeks AccountId = account.Id }; ctx.Transactions.Add(parent); ctx.SaveChanges(); parentId = parent.Id; } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId, startDate.AddDays(365)); using var verifyCtx = factory.CreateContext(); var children = await verifyCtx.Transactions .Where(t => t.ParentTransactionId == parentId) .ToListAsync(); Assert.Equal(2, children.Count); } [Fact] public async Task MaterializeRecurringTransactionsAsync_IgnoresNonParentRecurring() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { var account = SeedAccount(ctx); // A non-recurring transaction ctx.Transactions.Add(new Transaction { Description = "Single", Amount = 10, Date = DateTime.Today, RecurrenceType = RecurrenceType.None, AccountId = account.Id }); ctx.SaveChanges(); } using var context = factory.CreateContext(); var service = new TransactionService(context); await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); Assert.Single(verifyCtx.Transactions); } #endregion #region GetNextOccurrence [Theory] [InlineData(RecurrenceType.Daily, 1, 0, 0)] [InlineData(RecurrenceType.Weekly, 7, 0, 0)] [InlineData(RecurrenceType.Monthly, 0, 1, 0)] [InlineData(RecurrenceType.Yearly, 0, 0, 1)] public void GetNextOccurrence_ReturnsCorrectDate(RecurrenceType type, int expectedDays, int expectedMonths, int expectedYears) { var current = new DateTime(2025, 6, 15); var expected = current.AddDays(expectedDays).AddMonths(expectedMonths).AddYears(expectedYears); var result = TransactionService.GetNextOccurrence(current, type); Assert.Equal(expected, result); } [Fact] public void GetNextOccurrence_None_ReturnsMaxValue() { var result = TransactionService.GetNextOccurrence(DateTime.Today, RecurrenceType.None); Assert.Equal(DateTime.MaxValue, result); } #endregion }