Files
duempelkas/src/Duempelkas.Infrastructure/Services/AccountService.cs
T

200 lines
8.6 KiB
C#

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 AccountService : IAccountService
{
private readonly IDbContextFactory<FinanceDbContext> _dbFactory;
private readonly DatabaseLocation? _location;
public AccountService(IDbContextFactory<FinanceDbContext> dbFactory, DatabaseLocation? location = null)
{
_dbFactory = dbFactory;
_location = location;
}
public Task<List<AccountSummaryDto>> GetAllAccountsAsync()
{
return GetAllAccountsAsync(null);
}
public async Task<List<AccountSummaryDto>> GetAllAccountsAsync(int? year)
{
using var lease = await DatabaseLease.AcquireAsync(_location);
await using var db = await _dbFactory.CreateDbContextAsync();
var accounts = await db.Accounts
.OrderBy(a => a.Name)
.ToListAsync();
var entries = db.Entries.Where(e => !e.IsDeleted);
if (year.HasValue)
{
var movementBeforeYearByAccountId = await entries
.Where(e => e.Date.Year < year.Value)
.GroupBy(e => e.AccountId)
.ToDictionaryAsync(
g => g.Key,
g => g.Sum(e => e.Type == EntryType.Income ? e.Amount : -e.Amount));
var movementInYearByAccountId = await entries
.Where(e => e.Date.Year == year.Value)
.GroupBy(e => e.AccountId)
.ToDictionaryAsync(
g => g.Key,
g => g.Sum(e => e.Type == EntryType.Income ? e.Amount : -e.Amount));
return accounts
.Select(account =>
{
var carryoverForYear = account.CarryoverBalance + movementBeforeYearByAccountId.GetValueOrDefault(account.Id);
var totalBalance = carryoverForYear + movementInYearByAccountId.GetValueOrDefault(account.Id);
return new AccountSummaryDto(account.Id, account.Name, carryoverForYear, totalBalance, account.CreatedUtc);
})
.ToList();
}
var movementAllByAccountId = await entries
.GroupBy(e => e.AccountId)
.ToDictionaryAsync(
g => g.Key,
g => g.Sum(e => e.Type == EntryType.Income ? e.Amount : -e.Amount));
return accounts
.Select(account =>
{
var totalBalance = account.CarryoverBalance + movementAllByAccountId.GetValueOrDefault(account.Id);
return new AccountSummaryDto(account.Id, account.Name, account.CarryoverBalance, totalBalance, account.CreatedUtc);
})
.ToList();
}
public async Task<AccountSummaryDto> GetAccountAsync(int accountId)
{
using var lease = await DatabaseLease.AcquireAsync(_location);
await using var db = await _dbFactory.CreateDbContextAsync();
var account = await db.Accounts
.Include(a => a.Entries)
.FirstOrDefaultAsync(a => a.Id == accountId)
?? throw new InvalidOperationException($"Account {accountId} not found.");
var totalIncome = account.Entries.Where(e => !e.IsDeleted && e.Type == EntryType.Income).Sum(e => e.Amount);
var totalExpense = account.Entries.Where(e => !e.IsDeleted && e.Type == EntryType.Expense).Sum(e => e.Amount);
var totalBalance = account.CarryoverBalance + totalIncome - totalExpense;
return new AccountSummaryDto(account.Id, account.Name, account.CarryoverBalance, totalBalance, account.CreatedUtc);
}
public Task<AccountSummaryDto> CreateAccountAsync(string name) => CreateAccountAsync(name, 0m);
public async Task<AccountSummaryDto> CreateAccountAsync(string name, decimal initialBalance)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Bitte einen Kontonamen eingeben.");
if (decimal.Round(initialBalance, 2) != initialBalance) throw new ArgumentException("Ungültiger Anfangsbestand.");
using var lease = await DatabaseLease.AcquireAsync(_location);
await using var db = await _dbFactory.CreateDbContextAsync();
var account = new Account { Name = name.Trim(), CarryoverBalance = initialBalance };
db.Accounts.Add(account);
await db.SaveChangesAsync();
return new AccountSummaryDto(account.Id, account.Name, initialBalance, initialBalance, account.CreatedUtc);
}
public async Task RenameAccountAsync(int accountId, string newName)
{
using var lease = await DatabaseLease.AcquireAsync(_location);
await using var db = await _dbFactory.CreateDbContextAsync();
var account = await db.Accounts.FindAsync(accountId)
?? throw new InvalidOperationException($"Account {accountId} not found.");
account.Name = newName;
await db.SaveChangesAsync();
}
public async Task UpdateCarryoverAsync(int accountId, decimal carryoverBalance)
{
using var lease = await DatabaseLease.AcquireAsync(_location);
await using var db = await _dbFactory.CreateDbContextAsync();
var account = await db.Accounts.FindAsync(accountId)
?? throw new InvalidOperationException($"Account {accountId} not found.");
account.CarryoverBalance = carryoverBalance;
await db.SaveChangesAsync();
}
public async Task UpdateCarryoverAsync(int accountId, decimal carryoverBalance, int? year)
{
if (!year.HasValue)
{
await UpdateCarryoverAsync(accountId, carryoverBalance);
return;
}
using var lease = await DatabaseLease.AcquireAsync(_location);
await using var db = await _dbFactory.CreateDbContextAsync();
var account = await db.Accounts.FindAsync(accountId)
?? throw new InvalidOperationException($"Account {accountId} not found.");
var movementBeforeYear = await db.Entries
.Where(e => e.AccountId == accountId && !e.IsDeleted && e.Date.Year < year.Value)
.SumAsync(e => e.Type == EntryType.Income ? e.Amount : -e.Amount);
account.CarryoverBalance = carryoverBalance - movementBeforeYear;
await db.SaveChangesAsync();
}
public async Task CorrectCarryoverAsync(int accountId, decimal value, int? year, decimal expectedCarryover, decimal expectedInitial)
{
if (decimal.Round(value, 2) != value) throw new ArgumentException("Ungültiger Bestand.");
using var lease = await DatabaseLease.AcquireAsync(_location);
await using var db = await _dbFactory.CreateDbContextAsync();
await using var transaction = await db.Database.BeginTransactionAsync();
var account = await db.Accounts.FindAsync(accountId) ?? throw new InvalidOperationException("Konto nicht mehr vorhanden.");
var movement = year.HasValue ? await db.Entries.Where(e => e.AccountId == accountId && !e.IsDeleted && e.Date.Year < year.Value)
.SumAsync(e => e.Type == EntryType.Income ? e.Amount : -e.Amount) : 0m;
if (account.CarryoverBalance != expectedInitial || account.CarryoverBalance + movement != expectedCarryover)
throw new InvalidOperationException("Der Bestand wurde inzwischen geändert. Bitte abbrechen und die Korrektur mit aktuellen Werten erneut öffnen.");
account.CarryoverBalance = value - movement;
await db.SaveChangesAsync();
await transaction.CommitAsync();
}
public async Task DeleteAccountAsync(int accountId)
{
using var lease = await DatabaseLease.AcquireAsync(_location);
await using var db = await _dbFactory.CreateDbContextAsync();
var account = await db.Accounts
.Include(a => a.Entries)
.FirstOrDefaultAsync(a => a.Id == accountId)
?? throw new InvalidOperationException($"Account {accountId} not found.");
var entryIds = account.Entries.Select(e => e.Id).ToList();
var transferLinks = await db.TransferLinks
.Where(tl => entryIds.Contains(tl.SourceEntryId) || entryIds.Contains(tl.TargetEntryId))
.ToListAsync();
foreach (var link in transferLinks)
{
var otherEntryId = entryIds.Contains(link.SourceEntryId) ? link.TargetEntryId : link.SourceEntryId;
var otherEntry = await db.Entries.FindAsync(otherEntryId);
if (otherEntry != null)
db.Entries.Remove(otherEntry);
}
db.TransferLinks.RemoveRange(transferLinks);
db.Accounts.Remove(account);
await db.SaveChangesAsync();
}
}