From 4666056e24609dd17033916b1839d012cf7176de Mon Sep 17 00:00:00 2001 From: beo3000 Date: Thu, 1 Jan 2026 14:18:34 +0100 Subject: [PATCH] add expenses, on day closing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Neue Methode CreateAbsentMemberExpensesAsync() (Zeile 322-421) - Prüft Club.ExpenseCalculation (None → skip) - Berechnet Average/Maximum aller Tages-Expenses - Findet Absent-Expense via ExpenseTriggerType.Absent - Erstellt PersonExpense für alle abwesenden Members 2. Aufruf in AdvanceStatusAsync() (Zeile 312-316) - Wird aufgerufen wenn Status → Closed wechselt - Vor SaveChangesAsync() → eine Transaktion --- src/Koogle.Application/Services/DayService.cs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/src/Koogle.Application/Services/DayService.cs b/src/Koogle.Application/Services/DayService.cs index 855038d..2a9ec76 100644 --- a/src/Koogle.Application/Services/DayService.cs +++ b/src/Koogle.Application/Services/DayService.cs @@ -309,10 +309,114 @@ public class DayService : IDayService _ => throw new InvalidOperationException($"Unknown day status: {day.Status}") }; + // Create absent member expenses when closing + if (day.Status == DayStatus.Closed) + { + await CreateAbsentMemberExpensesAsync(context, day.Id, ct); + } + day.ModifiedAt = DateTime.UtcNow; await context.SaveChangesAsync(ct); return await GetByIdAsync(day.Id, ct) ?? throw new InvalidOperationException("Failed to advance status."); } + + /// + /// Creates PersonExpense for absent members when day closes. + /// + private async Task CreateAbsentMemberExpensesAsync( + AppDbContext context, + Guid dayId, + CancellationToken ct) + { + // 1. Load day with club and participants + var day = await context.Days + .Include(d => d.Club) + .Include(d => d.DayPersons) + .FirstAsync(d => d.Id == dayId, ct); + + var club = day.Club; + + // 2. Skip if ExpenseCalculation is None + if (club.ExpenseCalculation == ExpenseCalculation.None) + return; + + // 3. Get all PersonExpense prices for this day + var dayExpensePrices = await context.PersonExpenses + .Where(pe => pe.DayId == dayId && !pe.IsDeleted) + .Select(pe => pe.Price) + .ToListAsync(ct); + + // 4. Edge case: No expenses - skip + if (dayExpensePrices.Count == 0) + return; + + // 5. Calculate price + decimal calculatedPrice = club.ExpenseCalculation switch + { + ExpenseCalculation.Average => Math.Round(dayExpensePrices.Average(), 2), + ExpenseCalculation.Maximum => dayExpensePrices.Max(), + _ => 0m + }; + + if (calculatedPrice <= 0) + return; + + // 6. Get Expense linked to Absent trigger + var absentExpense = await context.ExpenseTriggers + .Where(et => et.ClubId == club.Id + && et.Trigger.ExpenseTriggerType == ExpenseTriggerType.Absent + && !et.Expense.IsDeleted) + .Select(et => et.Expense) + .FirstOrDefaultAsync(ct); + + // 7. No Absent expense configured - skip + if (absentExpense is null) + return; + + // 8. Get all club members + var allMemberIds = await context.Persons + .Where(p => p.ClubId == club.Id + && p.PersonStatus == PersonStatus.Member + && !p.IsDeleted) + .Select(p => p.Id) + .ToListAsync(ct); + + // 9. Get participant IDs + var participantIds = day.DayPersons + .Select(dp => dp.PersonId) + .ToHashSet(); + + // 10. Absent members = members NOT in DayPersons + var absentMemberIds = allMemberIds + .Where(id => !participantIds.Contains(id)) + .ToList(); + + if (absentMemberIds.Count == 0) + return; + + // 11. Create PersonExpense for each absent member + foreach (var memberId in absentMemberIds) + { + var personExpense = new PersonExpense + { + Id = Guid.NewGuid(), + PersonId = memberId, + ExpenseId = absentExpense.Id, + DayId = dayId, + GameId = null, + ClubId = club.Id, + Name = absentExpense.Name, + Price = calculatedPrice, + ExpenseType = absentExpense.ExpenseType, + PersonExpenseStatus = PersonExpenseStatus.Open, + AssignedById = Guid.Empty, + CreatedAt = DateTime.UtcNow, + IsDeleted = false + }; + context.PersonExpenses.Add(personExpense); + } + // SaveChanges called by AdvanceStatusAsync + } }