277 lines
9.5 KiB
C#
277 lines
9.5 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 EntryService : IEntryService
|
|
{
|
|
private readonly IDbContextFactory<FinanceDbContext> _dbFactory;
|
|
|
|
public EntryService(IDbContextFactory<FinanceDbContext> dbFactory) => _dbFactory = dbFactory;
|
|
|
|
public Task<List<EntryDto>> GetEntriesAsync(int accountId, bool currentYearOnly)
|
|
{
|
|
var year = currentYearOnly ? DateTime.Now.Year : (int?)null;
|
|
return GetEntriesAsync(accountId, year);
|
|
}
|
|
|
|
public async Task<List<EntryDto>> 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<List<int>> 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<EntryDto> 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<string> 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}";
|
|
}
|
|
}
|