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 EntryServiceBookingTests : IDisposable { private readonly FinanceDbContext _db; private readonly EntryService _entryService; private readonly string _connectionString = $"Data Source=duempelkas-entry-tests-{Guid.NewGuid():N};Mode=Memory;Cache=Shared"; public EntryServiceBookingTests() { var options = new DbContextOptionsBuilder() .UseSqlite(_connectionString) .Options; _db = new FinanceDbContext(options); _db.Database.OpenConnection(); _db.Database.EnsureCreated(); var dbFactory = new TestDbContextFactory(options); _entryService = new EntryService(dbFactory); } [Fact] public async Task CreateEntry_AssignsSequentialDisplayIds_PerYear() { var accountA = new Account { Name = "Konto A" }; var accountB = new Account { Name = "Konto B" }; _db.Accounts.AddRange(accountA, accountB); await _db.SaveChangesAsync(); var first = await _entryService.CreateEntryAsync(accountA.Id, EntryType.Income, new DateTime(2026, 1, 10), "Einnahme A", 100m); var second = await _entryService.CreateEntryAsync(accountB.Id, EntryType.Expense, new DateTime(2026, 1, 11), "Ausgabe B", 20m); var third = await _entryService.CreateEntryAsync(accountA.Id, EntryType.Income, new DateTime(2027, 1, 1), "Neues Jahr", 5m); first.DisplayId.Should().Be("2026-001"); second.DisplayId.Should().Be("2026-002"); third.DisplayId.Should().Be("2027-001"); } [Fact] public async Task CreateEntry_AfterTransfer_UsesNextDisplayId() { var source = new Account { Name = "Barkasse" }; var target = new Account { Name = "Girokonto" }; _db.Accounts.AddRange(source, target); await _db.SaveChangesAsync(); await _entryService.CreateTransferAsync(source.Id, target.Id, new DateTime(2026, 3, 15), "Umbuchung", 500m); var booking = await _entryService.CreateEntryAsync(source.Id, EntryType.Income, new DateTime(2026, 3, 16), "Einzahlung", 50m); booking.DisplayId.Should().Be("2026-002"); } [Fact] public async Task UpdateEntry_UpdatesBookingFields() { var account = new Account { Name = "Konto" }; _db.Accounts.Add(account); await _db.SaveChangesAsync(); var entry = await _entryService.CreateEntryAsync(account.Id, EntryType.Expense, new DateTime(2026, 2, 2), "Alt", 10m); await _entryService.UpdateEntryAsync(entry.Id, new DateTime(2026, 2, 3), "Neu", 25m); _db.ChangeTracker.Clear(); var updated = await _db.Entries.SingleAsync(e => e.Id == entry.Id); updated.Date.Should().Be(new DateTime(2026, 2, 3)); updated.Title.Should().Be("Neu"); updated.Amount.Should().Be(25m); } [Fact] public async Task DeleteAndRestoreEntry_TogglesSoftDelete_ForBookingOnly() { var account = new Account { Name = "Konto" }; _db.Accounts.Add(account); await _db.SaveChangesAsync(); var entry = await _entryService.CreateEntryAsync(account.Id, EntryType.Expense, new DateTime(2026, 2, 2), "Buchung", 10m); await _entryService.DeleteEntryAsync(entry.Id); _db.ChangeTracker.Clear(); var deleted = await _db.Entries.SingleAsync(e => e.Id == entry.Id); deleted.IsDeleted.Should().BeTrue(); await _entryService.RestoreEntryAsync(entry.Id); _db.ChangeTracker.Clear(); var restored = await _db.Entries.SingleAsync(e => e.Id == entry.Id); restored.IsDeleted.Should().BeFalse(); } 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)); } }