From 595c92df76966555ab835c4d295f30c6e03cf0af Mon Sep 17 00:00:00 2001 From: beo3000 Date: Sat, 3 Jan 2026 15:00:42 +0100 Subject: [PATCH] K10: create penalty entries on day close - ICashBookService.CreatePenaltyEntriesForDayAsync - Groups PersonExpenses by person, sums amounts - Marks PersonExpenses as Done - DayService calls after SaveChanges --- docs/IMPLEMENTATION_PLAN.md | 2 +- .../Interfaces/ICashBookService.cs | 8 ++ .../Services/CashBookService.cs | 76 +++++++++++++++++++ src/Koogle.Application/Services/DayService.cs | 18 +++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 38dd2d5..50c0f3c 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1708,7 +1708,7 @@ public enum CashBookEntryType { Income = 0, Expense = 1 } | ✓ | K7 | Application | Service Interfaces | 2 | | ✓ | K8 | Application | Service Implementations | 4 | | ✓ | K9 | Application | Category Seeder | 1 | -| ☐ | K10 | Application | Day Close Integration | 1 | +| ✓ | K10 | Application | Day Close Integration | 1 | | ☐ | K11 | Web | Fluxor CategoryState | 4 | | ☐ | K12 | Web | Fluxor CashBookState | 4 | | ☐ | K13 | Web | CashBook UI | 3 | diff --git a/src/Koogle.Application/Interfaces/ICashBookService.cs b/src/Koogle.Application/Interfaces/ICashBookService.cs index 8dd7194..f349ef9 100644 --- a/src/Koogle.Application/Interfaces/ICashBookService.cs +++ b/src/Koogle.Application/Interfaces/ICashBookService.cs @@ -89,4 +89,12 @@ public interface ICashBookService /// Cancellation token. /// True if fees exist; otherwise, false. Task MembershipFeesExistAsync(int year, int month, CancellationToken ct = default); + + /// + /// Creates cash book entries for monetary penalties from a closed day. + /// + /// The day identifier. + /// Cancellation token. + /// The number of entries created. + Task CreatePenaltyEntriesForDayAsync(Guid dayId, CancellationToken ct = default); } diff --git a/src/Koogle.Application/Services/CashBookService.cs b/src/Koogle.Application/Services/CashBookService.cs index a781c95..d62604f 100644 --- a/src/Koogle.Application/Services/CashBookService.cs +++ b/src/Koogle.Application/Services/CashBookService.cs @@ -246,4 +246,80 @@ public class CashBookService : ICashBookService { return await _entryRepository.ExistsMembershipFeeForMonthAsync(_clubContext.ClubId, year, month, ct); } + + /// + public async Task CreatePenaltyEntriesForDayAsync(Guid dayId, CancellationToken ct = default) + { + await using var context = await _contextFactory.CreateDbContextAsync(ct); + + // Get penalty category + var penaltyCategory = await _categoryRepository.GetSystemCategoryAsync(_clubContext.ClubId, "Spielstrafe", ct); + if (penaltyCategory is null) + { + return 0; // No category, skip silently + } + + // Get all monetary PersonExpenses for the day that are still open + var personExpenses = await context.PersonExpenses + .Include(pe => pe.Person) + .Where(pe => pe.DayId == dayId + && pe.ClubId == _clubContext.ClubId + && pe.ExpenseType == ExpenseType.Monetary + && pe.PersonExpenseStatus == PersonExpenseStatus.Open + && !pe.IsDeleted) + .ToListAsync(ct); + + if (personExpenses.Count == 0) + { + return 0; + } + + // Get day for booking date + var day = await context.Days.FirstOrDefaultAsync(d => d.Id == dayId, ct); + var bookingDate = day?.PostDate ?? DateTime.UtcNow; + + // Group by person and create entries + var groupedByPerson = personExpenses.GroupBy(pe => pe.PersonId); + var entriesCreated = 0; + + foreach (var group in groupedByPerson) + { + var personId = group.Key; + var personName = group.First().Person?.Name ?? "Unbekannt"; + var totalAmount = group.Sum(pe => pe.Price); + + if (totalAmount <= 0) + { + continue; + } + + // Create cash book entry + var entry = new CashBookEntry + { + Id = Guid.NewGuid(), + ClubId = _clubContext.ClubId, + CategoryId = penaltyCategory.Id, + EntryType = CashBookEntryType.Income, + Amount = totalAmount, + BookingDate = bookingDate, + Comment = $"Spielstrafen - {personName}", + DayId = dayId, + PersonId = personId, + CreatedAt = DateTime.UtcNow + }; + + await _entryRepository.AddAsync(entry, ct); + entriesCreated++; + + // Mark PersonExpenses as Done + foreach (var pe in group) + { + pe.PersonExpenseStatus = PersonExpenseStatus.Done; + pe.ModifiedAt = DateTime.UtcNow; + } + } + + await context.SaveChangesAsync(ct); + return entriesCreated; + } } diff --git a/src/Koogle.Application/Services/DayService.cs b/src/Koogle.Application/Services/DayService.cs index e89c28b..1717400 100644 --- a/src/Koogle.Application/Services/DayService.cs +++ b/src/Koogle.Application/Services/DayService.cs @@ -21,6 +21,7 @@ public class DayService : IDayService private readonly ICurrentClubContext _clubContext; private readonly IMapper _mapper; private readonly IEmailService _emailService; + private readonly ICashBookService _cashBookService; private readonly ILogger _logger; /// @@ -33,6 +34,7 @@ public class DayService : IDayService ICurrentClubContext clubContext, IMapper mapper, IEmailService emailService, + ICashBookService cashBookService, ILogger logger) { _contextFactory = contextFactory; @@ -41,6 +43,7 @@ public class DayService : IDayService _clubContext = clubContext; _mapper = mapper; _emailService = emailService; + _cashBookService = cashBookService; _logger = logger; } @@ -320,6 +323,21 @@ public class DayService : IDayService day.ModifiedAt = DateTime.UtcNow; await context.SaveChangesAsync(ct); + // Create cash book entries for penalties when closing + if (day.Status == DayStatus.Closed) + { + try + { + var entriesCreated = await _cashBookService.CreatePenaltyEntriesForDayAsync(day.Id, ct); + _logger.LogInformation("Created {Count} cash book entries for day {DayId}", entriesCreated, day.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create cash book entries for day {DayId}", day.Id); + // Don't rethrow - cashbook failure should not fail day closing + } + } + // Send protocol emails after closing (fire and forget, log errors) if (day.Status == DayStatus.Closed) {