K5: add cashbook repositories
- IBookingCategoryRepository + implementation - ICashBookEntryRepository + implementation - GetBalanceAsync calculates income - expense + initial - ExistsMembershipFeeForMonthAsync for duplicate check 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2f2e93ffce
commit
9c4a6f2ab6
|
|
@ -1703,7 +1703,7 @@ public enum CashBookEntryType { Income = 0, Expense = 1 }
|
|||
| ✓ | K2 | Infrastructure | EF Configurations | 4 |
|
||||
| ✓ | K3 | Infrastructure | Migration | - |
|
||||
| ✓ | K4 | Security | Kassenwart Role + Policy | 4 |
|
||||
| ☐ | K5 | Infrastructure | Repositories | 5 |
|
||||
| ✓ | K5 | Infrastructure | Repositories | 5 |
|
||||
| ☐ | K6 | Application | DTOs | 3 |
|
||||
| ☐ | K7 | Application | Service Interfaces | 2 |
|
||||
| ☐ | K8 | Application | Service Implementations | 4 |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
using Koogle.Domain.Entities;
|
||||
|
||||
namespace Koogle.Domain.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for booking category operations.
|
||||
/// </summary>
|
||||
public interface IBookingCategoryRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all booking categories for a club.
|
||||
/// </summary>
|
||||
Task<List<BookingCategory>> GetByClubIdAsync(Guid clubId, bool includeInactive = false, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a booking category by its identifier.
|
||||
/// </summary>
|
||||
Task<BookingCategory?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a system category by name for a club.
|
||||
/// </summary>
|
||||
Task<BookingCategory?> GetSystemCategoryAsync(Guid clubId, string name, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new booking category.
|
||||
/// </summary>
|
||||
Task<BookingCategory> AddAsync(BookingCategory entity, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing booking category.
|
||||
/// </summary>
|
||||
Task<BookingCategory> UpdateAsync(BookingCategory entity, CancellationToken ct = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
using Koogle.Domain.Entities;
|
||||
|
||||
namespace Koogle.Domain.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for cash book entry operations.
|
||||
/// </summary>
|
||||
public interface ICashBookEntryRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets cash book entries for a club within optional date range.
|
||||
/// </summary>
|
||||
Task<List<CashBookEntry>> GetByClubIdAsync(Guid clubId, DateTime? from, DateTime? to, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cash book entry by its identifier.
|
||||
/// </summary>
|
||||
Task<CashBookEntry?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the balance for a club as of a specific date.
|
||||
/// </summary>
|
||||
Task<decimal> GetBalanceAsync(Guid clubId, DateTime? asOfDate = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new cash book entry.
|
||||
/// </summary>
|
||||
Task<CashBookEntry> AddAsync(CashBookEntry entity, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets cash book entries linked to a specific day.
|
||||
/// </summary>
|
||||
Task<List<CashBookEntry>> GetByDayIdAsync(Guid dayId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a membership fee entry exists for a specific month.
|
||||
/// </summary>
|
||||
Task<bool> ExistsMembershipFeeForMonthAsync(Guid clubId, int year, int month, CancellationToken ct = default);
|
||||
}
|
||||
|
|
@ -92,6 +92,8 @@ public static class DependencyInjection
|
|||
services.AddScoped<ITriggerRepository, TriggerRepository>();
|
||||
services.AddScoped<IPlayerGameStatisticsRepository, PlayerGameStatisticsRepository>();
|
||||
services.AddScoped<IClubGifRepository, ClubGifRepository>();
|
||||
services.AddScoped<IBookingCategoryRepository, BookingCategoryRepository>();
|
||||
services.AddScoped<ICashBookEntryRepository, CashBookEntryRepository>();
|
||||
|
||||
// Services
|
||||
services.AddScoped<IEmailService, SmtpEmailService>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
using Koogle.Domain.Entities;
|
||||
using Koogle.Domain.Interfaces;
|
||||
using KoogleApp.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Koogle.Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing booking category entities.
|
||||
/// </summary>
|
||||
/// <param name="contextFactory">The database context factory for creating scoped contexts.</param>
|
||||
public class BookingCategoryRepository(IDbContextFactory<AppDbContext> contextFactory) : IBookingCategoryRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<List<BookingCategory>> GetByClubIdAsync(Guid clubId, bool includeInactive = false, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
var query = context.BookingCategories
|
||||
.Where(c => c.ClubId == clubId && !c.IsDeleted);
|
||||
|
||||
if (!includeInactive)
|
||||
{
|
||||
query = query.Where(c => c.IsActive);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(c => c.Name)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BookingCategory?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.BookingCategories
|
||||
.FirstOrDefaultAsync(c => c.Id == id && !c.IsDeleted, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BookingCategory?> GetSystemCategoryAsync(Guid clubId, string name, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.BookingCategories
|
||||
.FirstOrDefaultAsync(c => c.ClubId == clubId && c.IsSystemCategory && c.Name == name && !c.IsDeleted, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BookingCategory> AddAsync(BookingCategory entity, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
entity.Id = Guid.NewGuid();
|
||||
entity.CreatedAt = DateTime.UtcNow;
|
||||
await context.BookingCategories.AddAsync(entity, ct);
|
||||
await context.SaveChangesAsync(ct);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BookingCategory> UpdateAsync(BookingCategory entity, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
entity.ModifiedAt = DateTime.UtcNow;
|
||||
context.BookingCategories.Update(entity);
|
||||
await context.SaveChangesAsync(ct);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
using Koogle.Domain.Entities;
|
||||
using Koogle.Domain.Enums;
|
||||
using Koogle.Domain.Interfaces;
|
||||
using KoogleApp.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Koogle.Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing cash book entry entities.
|
||||
/// </summary>
|
||||
/// <param name="contextFactory">The database context factory for creating scoped contexts.</param>
|
||||
public class CashBookEntryRepository(IDbContextFactory<AppDbContext> contextFactory) : ICashBookEntryRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<List<CashBookEntry>> GetByClubIdAsync(Guid clubId, DateTime? from, DateTime? to, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
IQueryable<CashBookEntry> query = context.CashBookEntries
|
||||
.Where(e => e.ClubId == clubId && !e.IsDeleted);
|
||||
|
||||
if (from.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.BookingDate >= from.Value);
|
||||
}
|
||||
|
||||
if (to.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.BookingDate <= to.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.Include(e => e.Category)
|
||||
.Include(e => e.Person)
|
||||
.Include(e => e.Day)
|
||||
.OrderByDescending(e => e.BookingDate)
|
||||
.ThenByDescending(e => e.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CashBookEntry?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.CashBookEntries
|
||||
.Include(e => e.Category)
|
||||
.Include(e => e.Person)
|
||||
.Include(e => e.Day)
|
||||
.FirstOrDefaultAsync(e => e.Id == id && !e.IsDeleted, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<decimal> GetBalanceAsync(Guid clubId, DateTime? asOfDate = null, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// Get initial balance from club
|
||||
var club = await context.Clubs.FirstOrDefaultAsync(c => c.Id == clubId, ct);
|
||||
var initialBalance = club?.InitialBalance ?? 0m;
|
||||
|
||||
var query = context.CashBookEntries
|
||||
.Where(e => e.ClubId == clubId && !e.IsDeleted);
|
||||
|
||||
if (asOfDate.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.BookingDate <= asOfDate.Value);
|
||||
}
|
||||
|
||||
var entries = await query.ToListAsync(ct);
|
||||
|
||||
var income = entries
|
||||
.Where(e => e.EntryType == CashBookEntryType.Income)
|
||||
.Sum(e => e.Amount);
|
||||
|
||||
var expense = entries
|
||||
.Where(e => e.EntryType == CashBookEntryType.Expense)
|
||||
.Sum(e => e.Amount);
|
||||
|
||||
return initialBalance + income - expense;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CashBookEntry> AddAsync(CashBookEntry entity, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
entity.Id = Guid.NewGuid();
|
||||
entity.CreatedAt = DateTime.UtcNow;
|
||||
await context.CashBookEntries.AddAsync(entity, ct);
|
||||
await context.SaveChangesAsync(ct);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<CashBookEntry>> GetByDayIdAsync(Guid dayId, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.CashBookEntries
|
||||
.Where(e => e.DayId == dayId && !e.IsDeleted)
|
||||
.Include(e => e.Category)
|
||||
.Include(e => e.Person)
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> ExistsMembershipFeeForMonthAsync(Guid clubId, int year, int month, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// Get the system category for membership fees
|
||||
var membershipCategory = await context.BookingCategories
|
||||
.FirstOrDefaultAsync(c => c.ClubId == clubId && c.IsSystemCategory && c.Name == "Mitgliedsbeitrag" && !c.IsDeleted, ct);
|
||||
|
||||
if (membershipCategory == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var startOfMonth = new DateTime(year, month, 1);
|
||||
var endOfMonth = startOfMonth.AddMonths(1).AddDays(-1);
|
||||
|
||||
return await context.CashBookEntries
|
||||
.AnyAsync(e => e.ClubId == clubId
|
||||
&& e.CategoryId == membershipCategory.Id
|
||||
&& e.BookingDate >= startOfMonth
|
||||
&& e.BookingDate <= endOfMonth
|
||||
&& !e.IsDeleted, ct);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue