add expenses, on day closing

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
This commit is contained in:
beo3000 2026-01-01 14:18:34 +01:00
parent 8fd8c37952
commit 4666056e24
1 changed files with 104 additions and 0 deletions

View File

@ -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.");
}
/// <summary>
/// Creates PersonExpense for absent members when day closes.
/// </summary>
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
}
}