using Duempelkas.Domain.Entities; using Duempelkas.Domain.Enums; using Duempelkas.Infrastructure.Persistence; using Duempelkas.Infrastructure.Services; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Xunit; namespace Duempelkas.Tests; public class TransferServiceTests : IDisposable { private readonly FinanceDbContext _db; private readonly EntryService _entryService; private readonly BalanceQueryService _balanceQueryService; public TransferServiceTests() { var options = new DbContextOptionsBuilder() .UseSqlite("Data Source=duempelkas-transfer-tests;Mode=Memory;Cache=Shared") .Options; _db = new FinanceDbContext(options); _db.Database.OpenConnection(); _db.Database.EnsureCreated(); var dbFactory = new TestDbContextFactory(options); _entryService = new EntryService(dbFactory); _balanceQueryService = new BalanceQueryService(dbFactory); } [Fact] public async Task CreateTransfer_CreatesLinkedExpenseAndIncome() { var accountA = new Account { Name = "Account A" }; var accountB = new Account { Name = "Account B" }; _db.Accounts.AddRange(accountA, accountB); await _db.SaveChangesAsync(); await _entryService.CreateTransferAsync(accountA.Id, accountB.Id, new DateTime(2026, 3, 15), "Test Transfer", 100.00m); var entries = await _db.Entries.ToListAsync(); entries.Should().HaveCount(2); var sourceEntry = entries.Single(e => e.AccountId == accountA.Id); sourceEntry.Type.Should().Be(EntryType.Expense); sourceEntry.Amount.Should().Be(100.00m); sourceEntry.Title.Should().Be("Test Transfer"); sourceEntry.DisplayId.Should().Be("2026-001"); var targetEntry = entries.Single(e => e.AccountId == accountB.Id); targetEntry.Type.Should().Be(EntryType.Income); targetEntry.Amount.Should().Be(100.00m); targetEntry.DisplayId.Should().Be("2026-002"); var links = await _db.TransferLinks.ToListAsync(); links.Should().HaveCount(1); links[0].SourceEntryId.Should().Be(sourceEntry.Id); links[0].TargetEntryId.Should().Be(targetEntry.Id); var balanceA = await _balanceQueryService.GetAccountBalanceAsync(accountA.Id); balanceA.TotalBalance.Should().Be(-100.00m); var balanceB = await _balanceQueryService.GetAccountBalanceAsync(accountB.Id); balanceB.TotalBalance.Should().Be(100.00m); } [Fact] public async Task DeleteTransfer_SoftDeletesBothSides() { var accountA = new Account { Name = "Account A" }; var accountB = new Account { Name = "Account B" }; _db.Accounts.AddRange(accountA, accountB); await _db.SaveChangesAsync(); await _entryService.CreateTransferAsync(accountA.Id, accountB.Id, new DateTime(2026, 3, 15), "Test Transfer", 100.00m); var sourceEntry = await _db.Entries.FirstAsync(e => e.AccountId == accountA.Id); await _entryService.DeleteEntryAsync(sourceEntry.Id); _db.ChangeTracker.Clear(); var entries = await _db.Entries.ToListAsync(); entries.Should().HaveCount(2); entries.Should().AllSatisfy(e => e.IsDeleted.Should().BeTrue()); var links = await _db.TransferLinks.ToListAsync(); links.Should().HaveCount(1); var balanceA = await _balanceQueryService.GetAccountBalanceAsync(accountA.Id); balanceA.TotalBalance.Should().Be(0m); var balanceB = await _balanceQueryService.GetAccountBalanceAsync(accountB.Id); balanceB.TotalBalance.Should().Be(0m); } [Fact] public async Task CreateTransfer_SameAccount_Throws() { var account = new Account { Name = "Account A" }; _db.Accounts.Add(account); await _db.SaveChangesAsync(); var act = () => _entryService.CreateTransferAsync(account.Id, account.Id, DateTime.Today, "Bad Transfer", 50m); await act.Should().ThrowAsync(); } public void Dispose() { _db.Database.CloseConnection(); _db.Dispose(); } private sealed class TestDbContextFactory : IDbContextFactory { private readonly DbContextOptions _options; public TestDbContextFactory(DbContextOptions options) { _options = options; } public FinanceDbContext CreateDbContext() => new(_options); public Task CreateDbContextAsync(CancellationToken cancellationToken = default) => Task.FromResult(new FinanceDbContext(_options)); } }