using Duempelkas.App.Services; using Duempelkas.App.Services.Models; using Duempelkas.Domain.Entities; using Duempelkas.Domain.Enums; using Duempelkas.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; namespace Duempelkas.Infrastructure.Services; public class EntryService : IEntryService { private readonly IDbContextFactory _dbFactory; public EntryService(IDbContextFactory dbFactory) => _dbFactory = dbFactory; public Task> GetEntriesAsync(int accountId, bool currentYearOnly) { var year = currentYearOnly ? DateTime.Now.Year : (int?)null; return GetEntriesAsync(accountId, year); } public async Task> GetEntriesAsync(int accountId, int? year) { await using var db = await _dbFactory.CreateDbContextAsync(); var query = db.Entries.Where(e => e.AccountId == accountId); if (year.HasValue) query = query.Where(e => e.Date.Year == year.Value); var entries = await query .OrderBy(e => e.Date) .ThenBy(e => e.CreatedUtc) .ToListAsync(); var entryIds = entries.Select(e => e.Id).ToList(); var transferLinks = await db.TransferLinks .Include(tl => tl.SourceEntry).ThenInclude(e => e.Account) .Include(tl => tl.TargetEntry).ThenInclude(e => e.Account) .Where(tl => entryIds.Contains(tl.SourceEntryId) || entryIds.Contains(tl.TargetEntryId)) .ToListAsync(); return entries.Select(e => { var link = transferLinks.FirstOrDefault(tl => tl.SourceEntryId == e.Id || tl.TargetEntryId == e.Id); string? linkedAccountName = null; int? linkedAccountId = null; if (link != null) { if (link.SourceEntryId == e.Id) { linkedAccountName = link.TargetEntry.Account.Name; linkedAccountId = link.TargetEntry.AccountId; } else { linkedAccountName = link.SourceEntry.Account.Name; linkedAccountId = link.SourceEntry.AccountId; } } return new EntryDto( e.Id, e.AccountId, e.DisplayId, e.Type, e.Date, e.Title, e.Amount, e.IsDeleted, link != null, link?.Id, linkedAccountName, linkedAccountId); }).ToList(); } public async Task> GetEntryYearsAsync(int accountId) { await using var db = await _dbFactory.CreateDbContextAsync(); return await db.Entries .Where(e => e.AccountId == accountId) .Select(e => e.Date.Year) .Distinct() .OrderByDescending(y => y) .ToListAsync(); } public async Task CreateEntryAsync(int accountId, EntryType type, DateTime date, string title, decimal amount) { await using var db = await _dbFactory.CreateDbContextAsync(); var displayId = await GenerateDisplayIdAsync(db, date.Year); var entry = new Entry { AccountId = accountId, DisplayId = displayId, Type = type, Date = date, Title = title, Amount = amount }; db.Entries.Add(entry); await db.SaveChangesAsync(); return new EntryDto(entry.Id, entry.AccountId, entry.DisplayId, entry.Type, entry.Date, entry.Title, entry.Amount, false, false, null, null, null); } public async Task CreateTransferAsync(int sourceAccountId, int targetAccountId, DateTime date, string title, decimal amount) { if (sourceAccountId == targetAccountId) throw new InvalidOperationException("Umbuchung innerhalb desselben Kontos ist nicht möglich."); await using var db = await _dbFactory.CreateDbContextAsync(); await using var transaction = await db.Database.BeginTransactionAsync(); var transferDisplayId = await GenerateDisplayIdAsync(db, date.Year); var sourceEntry = new Entry { AccountId = sourceAccountId, DisplayId = transferDisplayId, Type = EntryType.Expense, Date = date, Title = title, Amount = amount }; db.Entries.Add(sourceEntry); await db.SaveChangesAsync(); var targetEntry = new Entry { AccountId = targetAccountId, DisplayId = transferDisplayId, Type = EntryType.Income, Date = date, Title = title, Amount = amount }; db.Entries.Add(targetEntry); await db.SaveChangesAsync(); var link = new TransferLink { SourceEntryId = sourceEntry.Id, TargetEntryId = targetEntry.Id, Note = $"Umbuchung: {title}" }; db.TransferLinks.Add(link); await db.SaveChangesAsync(); sourceEntry.TransferLinkId = link.Id; targetEntry.TransferLinkId = link.Id; await db.SaveChangesAsync(); await transaction.CommitAsync(); } public async Task DeleteEntryAsync(int entryId) { await using var db = await _dbFactory.CreateDbContextAsync(); var entry = await db.Entries.FindAsync(entryId) ?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden."); entry.IsDeleted = true; var link = await db.TransferLinks .FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId); if (link != null) { var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId; var otherEntry = await db.Entries.FindAsync(otherEntryId); if (otherEntry != null) otherEntry.IsDeleted = true; } await db.SaveChangesAsync(); } public async Task RestoreEntryAsync(int entryId) { await using var db = await _dbFactory.CreateDbContextAsync(); var entry = await db.Entries.FindAsync(entryId) ?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden."); entry.IsDeleted = false; var link = await db.TransferLinks .FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId); if (link != null) { var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId; var otherEntry = await db.Entries.FindAsync(otherEntryId); if (otherEntry != null) otherEntry.IsDeleted = false; } await db.SaveChangesAsync(); } public async Task UpdateEntryAsync(int entryId, DateTime date, string title, decimal amount) { await using var db = await _dbFactory.CreateDbContextAsync(); var entry = await db.Entries.FindAsync(entryId) ?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden."); var link = await db.TransferLinks .FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId); entry.Date = date; entry.Title = title; entry.Amount = amount; if (link != null) { var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId; var otherEntry = await db.Entries.FindAsync(otherEntryId); if (otherEntry != null) { otherEntry.Date = date; otherEntry.Title = title; otherEntry.Amount = amount; } } await db.SaveChangesAsync(); } public async Task UpdateTransferAsync(int entryId, int newLinkedAccountId, DateTime date, string title, decimal amount) { await using var db = await _dbFactory.CreateDbContextAsync(); var entry = await db.Entries.FindAsync(entryId) ?? throw new InvalidOperationException($"Eintrag {entryId} nicht gefunden."); var link = await db.TransferLinks .FirstOrDefaultAsync(tl => tl.SourceEntryId == entryId || tl.TargetEntryId == entryId) ?? throw new InvalidOperationException($"Kein Transfer-Link für Eintrag {entryId} gefunden."); var otherEntryId = link.SourceEntryId == entryId ? link.TargetEntryId : link.SourceEntryId; var otherEntry = await db.Entries.FindAsync(otherEntryId) ?? throw new InvalidOperationException($"Gegeneintrag {otherEntryId} nicht gefunden."); // Update date, title, amount on both sides entry.Date = date; entry.Title = title; entry.Amount = amount; otherEntry.Date = date; otherEntry.Title = title; otherEntry.Amount = amount; // If the linked account has changed, move the other entry to the new account if (otherEntry.AccountId != newLinkedAccountId) { otherEntry.AccountId = newLinkedAccountId; } await db.SaveChangesAsync(); } private static async Task GenerateDisplayIdAsync(FinanceDbContext db, int year) { var prefix = $"{year}-"; var maxDisplayId = await db.Entries .Where(e => e.DisplayId.StartsWith(prefix)) .Select(e => e.DisplayId) .MaxAsync(id => (string?)id); int nextNumber = 1; if (maxDisplayId != null) { var parts = maxDisplayId.Split('-'); if (parts.Length == 2 && int.TryParse(parts[1], out var current)) nextNumber = current + 1; } return $"{year}-{nextNumber:D3}"; } }