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)
{