Add infrastructure layer with persistence and services

- EF Core DbContext and entity configurations
- Design-time factory for migrations
- Initial and soft-delete migrations
- Service implementations: Account, AccountYear, Entry,
  BalanceQuery, FileSave, PdfStatement
- Dependency injection registration
This commit is contained in:
2026-03-31 17:12:57 +02:00
parent 8fbb8b8ea2
commit c3d68020d5
19 changed files with 1450 additions and 0 deletions
@@ -0,0 +1,93 @@
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 FinanceDbContext _db;
public AccountService(FinanceDbContext db) => _db = db;
public async Task<List<AccountSummaryDto>> GetAllAccountsAsync()
{
var accounts = await _db.Accounts
.Include(a => a.Entries)
.OrderBy(a => a.Name)
.ToListAsync();
return accounts.Select(MapToSummary).ToList();
}
public async Task<AccountSummaryDto> GetAccountAsync(int accountId)
{
var account = await _db.Accounts
.Include(a => a.Entries)
.FirstOrDefaultAsync(a => a.Id == accountId)
?? throw new InvalidOperationException($"Account {accountId} not found.");
return MapToSummary(account);
}
public async Task<AccountSummaryDto> CreateAccountAsync(string name)
{
var account = new Account { Name = name };
_db.Accounts.Add(account);
await _db.SaveChangesAsync();
return new AccountSummaryDto(account.Id, account.Name, 0m, 0m, account.CreatedUtc);
}
public async Task RenameAccountAsync(int accountId, string newName)
{
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)
{
var account = await _db.Accounts.FindAsync(accountId)
?? throw new InvalidOperationException($"Account {accountId} not found.");
account.CarryoverBalance = carryoverBalance;
await _db.SaveChangesAsync();
}
public async Task DeleteAccountAsync(int accountId)
{
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();
}
private static AccountSummaryDto MapToSummary(Account account)
{
var totalIncome = account.Entries.Where(e => e.Type == EntryType.Income).Sum(e => e.Amount);
var totalExpense = account.Entries.Where(e => 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);
}
}