K10: create penalty entries on day close
- ICashBookService.CreatePenaltyEntriesForDayAsync - Groups PersonExpenses by person, sums amounts - Marks PersonExpenses as Done - DayService calls after SaveChanges
This commit is contained in:
parent
21c3d03c61
commit
595c92df76
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -89,4 +89,12 @@ public interface ICashBookService
|
|||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>True if fees exist; otherwise, false.</returns>
|
||||
Task<bool> MembershipFeesExistAsync(int year, int month, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates cash book entries for monetary penalties from a closed day.
|
||||
/// </summary>
|
||||
/// <param name="dayId">The day identifier.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The number of entries created.</returns>
|
||||
Task<int> CreatePenaltyEntriesForDayAsync(Guid dayId, CancellationToken ct = default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,4 +246,80 @@ public class CashBookService : ICashBookService
|
|||
{
|
||||
return await _entryRepository.ExistsMembershipFeeForMonthAsync(_clubContext.ClubId, year, month, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DayService> _logger;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -33,6 +34,7 @@ public class DayService : IDayService
|
|||
ICurrentClubContext clubContext,
|
||||
IMapper mapper,
|
||||
IEmailService emailService,
|
||||
ICashBookService cashBookService,
|
||||
ILogger<DayService> 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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue