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 |
|
| ✓ | K7 | Application | Service Interfaces | 2 |
|
||||||
| ✓ | K8 | Application | Service Implementations | 4 |
|
| ✓ | K8 | Application | Service Implementations | 4 |
|
||||||
| ✓ | K9 | Application | Category Seeder | 1 |
|
| ✓ | K9 | Application | Category Seeder | 1 |
|
||||||
| ☐ | K10 | Application | Day Close Integration | 1 |
|
| ✓ | K10 | Application | Day Close Integration | 1 |
|
||||||
| ☐ | K11 | Web | Fluxor CategoryState | 4 |
|
| ☐ | K11 | Web | Fluxor CategoryState | 4 |
|
||||||
| ☐ | K12 | Web | Fluxor CashBookState | 4 |
|
| ☐ | K12 | Web | Fluxor CashBookState | 4 |
|
||||||
| ☐ | K13 | Web | CashBook UI | 3 |
|
| ☐ | K13 | Web | CashBook UI | 3 |
|
||||||
|
|
|
||||||
|
|
@ -89,4 +89,12 @@ public interface ICashBookService
|
||||||
/// <param name="ct">Cancellation token.</param>
|
/// <param name="ct">Cancellation token.</param>
|
||||||
/// <returns>True if fees exist; otherwise, false.</returns>
|
/// <returns>True if fees exist; otherwise, false.</returns>
|
||||||
Task<bool> MembershipFeesExistAsync(int year, int month, CancellationToken ct = default);
|
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);
|
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 ICurrentClubContext _clubContext;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
private readonly IEmailService _emailService;
|
private readonly IEmailService _emailService;
|
||||||
|
private readonly ICashBookService _cashBookService;
|
||||||
private readonly ILogger<DayService> _logger;
|
private readonly ILogger<DayService> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -33,6 +34,7 @@ public class DayService : IDayService
|
||||||
ICurrentClubContext clubContext,
|
ICurrentClubContext clubContext,
|
||||||
IMapper mapper,
|
IMapper mapper,
|
||||||
IEmailService emailService,
|
IEmailService emailService,
|
||||||
|
ICashBookService cashBookService,
|
||||||
ILogger<DayService> logger)
|
ILogger<DayService> logger)
|
||||||
{
|
{
|
||||||
_contextFactory = contextFactory;
|
_contextFactory = contextFactory;
|
||||||
|
|
@ -41,6 +43,7 @@ public class DayService : IDayService
|
||||||
_clubContext = clubContext;
|
_clubContext = clubContext;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
_emailService = emailService;
|
_emailService = emailService;
|
||||||
|
_cashBookService = cashBookService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -320,6 +323,21 @@ public class DayService : IDayService
|
||||||
day.ModifiedAt = DateTime.UtcNow;
|
day.ModifiedAt = DateTime.UtcNow;
|
||||||
await context.SaveChangesAsync(ct);
|
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)
|
// Send protocol emails after closing (fire and forget, log errors)
|
||||||
if (day.Status == DayStatus.Closed)
|
if (day.Status == DayStatus.Closed)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue