diff --git a/tests/Duempelkas.Tests/Duempelkas.Tests.csproj b/tests/Duempelkas.Tests/Duempelkas.Tests.csproj
new file mode 100644
index 0000000..19a5933
--- /dev/null
+++ b/tests/Duempelkas.Tests/Duempelkas.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+ net10.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Duempelkas.Tests/TransferServiceTests.cs b/tests/Duempelkas.Tests/TransferServiceTests.cs
new file mode 100644
index 0000000..82fe310
--- /dev/null
+++ b/tests/Duempelkas.Tests/TransferServiceTests.cs
@@ -0,0 +1,110 @@
+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=:memory:")
+ .Options;
+
+ _db = new FinanceDbContext(options);
+ _db.Database.OpenConnection();
+ _db.Database.EnsureCreated();
+
+ _entryService = new EntryService(_db);
+ _balanceQueryService = new BalanceQueryService(_db);
+ }
+
+ [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);
+
+ 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();
+ }
+}
diff --git a/tests/Duempelkas.Tests/YearlyStatementCalculationTests.cs b/tests/Duempelkas.Tests/YearlyStatementCalculationTests.cs
new file mode 100644
index 0000000..9af8946
--- /dev/null
+++ b/tests/Duempelkas.Tests/YearlyStatementCalculationTests.cs
@@ -0,0 +1,109 @@
+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 BalanceCalculationTests : IDisposable
+{
+ private readonly FinanceDbContext _db;
+ private readonly BalanceQueryService _balanceQueryService;
+ private readonly EntryService _entryService;
+
+ public BalanceCalculationTests()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("Data Source=:memory:")
+ .Options;
+
+ _db = new FinanceDbContext(options);
+ _db.Database.OpenConnection();
+ _db.Database.EnsureCreated();
+
+ _balanceQueryService = new BalanceQueryService(_db);
+ _entryService = new EntryService(_db);
+ }
+
+ [Fact]
+ public async Task AccountBalance_CalculatesCorrectTotals()
+ {
+ var account = new Account { Name = "Test Account", CarryoverBalance = 500.00m };
+ _db.Accounts.Add(account);
+ await _db.SaveChangesAsync();
+
+ _db.Entries.AddRange(
+ new Entry { AccountId = account.Id, DisplayId = "2026-001", Type = EntryType.Income, Date = new DateTime(2026, 1, 15), Title = "Gehalt", Amount = 800.00m },
+ new Entry { AccountId = account.Id, DisplayId = "2026-002", Type = EntryType.Income, Date = new DateTime(2026, 2, 15), Title = "Bonus", Amount = 400.00m },
+ new Entry { AccountId = account.Id, DisplayId = "2026-003", Type = EntryType.Expense, Date = new DateTime(2026, 1, 20), Title = "Miete", Amount = 200.00m },
+ new Entry { AccountId = account.Id, DisplayId = "2026-004", Type = EntryType.Expense, Date = new DateTime(2026, 2, 20), Title = "Lebensmittel", Amount = 250.00m }
+ );
+ await _db.SaveChangesAsync();
+
+ var balance = await _balanceQueryService.GetAccountBalanceAsync(account.Id);
+
+ balance.CarryoverBalance.Should().Be(500.00m);
+ balance.TotalIncome.Should().Be(1200.00m);
+ balance.TotalExpense.Should().Be(450.00m);
+ balance.TotalBalance.Should().Be(1250.00m);
+ }
+
+ [Fact]
+ public async Task AccountBalance_IncludesTransfersCorrectly()
+ {
+ var accountA = new Account { Name = "Account A", CarryoverBalance = 500.00m };
+ var accountB = new Account { Name = "Account B" };
+ _db.Accounts.AddRange(accountA, accountB);
+ await _db.SaveChangesAsync();
+
+ _db.Entries.Add(new Entry
+ {
+ AccountId = accountA.Id,
+ DisplayId = "2026-001",
+ Type = EntryType.Income,
+ Date = new DateTime(2026, 1, 10),
+ Title = "Gehalt",
+ Amount = 1000.00m
+ });
+ await _db.SaveChangesAsync();
+
+ await _entryService.CreateTransferAsync(accountA.Id, accountB.Id, new DateTime(2026, 2, 1), "Umbuchung nach B", 300.00m);
+
+ var balanceA = await _balanceQueryService.GetAccountBalanceAsync(accountA.Id);
+
+ balanceA.CarryoverBalance.Should().Be(500.00m);
+ balanceA.TotalIncome.Should().Be(1000.00m);
+ balanceA.TotalExpense.Should().Be(300.00m);
+ balanceA.TotalBalance.Should().Be(1200.00m);
+ }
+
+ [Fact]
+ public async Task AccountBalance_WithCarryover()
+ {
+ var account = new Account { Name = "Konto mit Übertrag", CarryoverBalance = 1000.00m };
+ _db.Accounts.Add(account);
+ await _db.SaveChangesAsync();
+
+ _db.Entries.AddRange(
+ new Entry { AccountId = account.Id, DisplayId = "2025-001", Type = EntryType.Income, Date = new DateTime(2025, 6, 1), Title = "Einnahme 2025", Amount = 500.00m },
+ new Entry { AccountId = account.Id, DisplayId = "2026-001", Type = EntryType.Income, Date = new DateTime(2026, 6, 1), Title = "Einnahme 2026", Amount = 300.00m },
+ new Entry { AccountId = account.Id, DisplayId = "2026-002", Type = EntryType.Expense, Date = new DateTime(2026, 7, 1), Title = "Ausgabe 2026", Amount = 150.00m }
+ );
+ await _db.SaveChangesAsync();
+
+ var balance = await _balanceQueryService.GetAccountBalanceAsync(account.Id);
+
+ // 1000 + 500 + 300 - 150 = 1650
+ balance.TotalBalance.Should().Be(1650.00m);
+ balance.CarryoverBalance.Should().Be(1000.00m);
+ }
+
+ public void Dispose()
+ {
+ _db.Database.CloseConnection();
+ _db.Dispose();
+ }
+}