diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 6b945e7..63acc71 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -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 | diff --git a/src/Koogle.Domain/Interfaces/IBookingCategoryRepository.cs b/src/Koogle.Domain/Interfaces/IBookingCategoryRepository.cs new file mode 100644 index 0000000..9fe9de5 --- /dev/null +++ b/src/Koogle.Domain/Interfaces/IBookingCategoryRepository.cs @@ -0,0 +1,34 @@ +using Koogle.Domain.Entities; + +namespace Koogle.Domain.Interfaces; + +/// +/// Repository interface for booking category operations. +/// +public interface IBookingCategoryRepository +{ + /// + /// Gets all booking categories for a club. + /// + Task> GetByClubIdAsync(Guid clubId, bool includeInactive = false, CancellationToken ct = default); + + /// + /// Gets a booking category by its identifier. + /// + Task GetByIdAsync(Guid id, CancellationToken ct = default); + + /// + /// Gets a system category by name for a club. + /// + Task GetSystemCategoryAsync(Guid clubId, string name, CancellationToken ct = default); + + /// + /// Adds a new booking category. + /// + Task AddAsync(BookingCategory entity, CancellationToken ct = default); + + /// + /// Updates an existing booking category. + /// + Task UpdateAsync(BookingCategory entity, CancellationToken ct = default); +} diff --git a/src/Koogle.Domain/Interfaces/ICashBookEntryRepository.cs b/src/Koogle.Domain/Interfaces/ICashBookEntryRepository.cs new file mode 100644 index 0000000..db05f31 --- /dev/null +++ b/src/Koogle.Domain/Interfaces/ICashBookEntryRepository.cs @@ -0,0 +1,39 @@ +using Koogle.Domain.Entities; + +namespace Koogle.Domain.Interfaces; + +/// +/// Repository interface for cash book entry operations. +/// +public interface ICashBookEntryRepository +{ + /// + /// Gets cash book entries for a club within optional date range. + /// + Task> GetByClubIdAsync(Guid clubId, DateTime? from, DateTime? to, CancellationToken ct = default); + + /// + /// Gets a cash book entry by its identifier. + /// + Task GetByIdAsync(Guid id, CancellationToken ct = default); + + /// + /// Calculates the balance for a club as of a specific date. + /// + Task GetBalanceAsync(Guid clubId, DateTime? asOfDate = null, CancellationToken ct = default); + + /// + /// Adds a new cash book entry. + /// + Task AddAsync(CashBookEntry entity, CancellationToken ct = default); + + /// + /// Gets cash book entries linked to a specific day. + /// + Task> GetByDayIdAsync(Guid dayId, CancellationToken ct = default); + + /// + /// Checks if a membership fee entry exists for a specific month. + /// + Task ExistsMembershipFeeForMonthAsync(Guid clubId, int year, int month, CancellationToken ct = default); +} diff --git a/src/Koogle.Infrastructure/DependencyInjection.cs b/src/Koogle.Infrastructure/DependencyInjection.cs index 32f6ecd..a347b12 100644 --- a/src/Koogle.Infrastructure/DependencyInjection.cs +++ b/src/Koogle.Infrastructure/DependencyInjection.cs @@ -92,6 +92,8 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // Services services.AddScoped(); diff --git a/src/Koogle.Infrastructure/Repositories/BookingCategoryRepository.cs b/src/Koogle.Infrastructure/Repositories/BookingCategoryRepository.cs new file mode 100644 index 0000000..8f0c614 --- /dev/null +++ b/src/Koogle.Infrastructure/Repositories/BookingCategoryRepository.cs @@ -0,0 +1,67 @@ +using Koogle.Domain.Entities; +using Koogle.Domain.Interfaces; +using KoogleApp.Data; +using Microsoft.EntityFrameworkCore; + +namespace Koogle.Infrastructure.Repositories; + +/// +/// Repository implementation for managing booking category entities. +/// +/// The database context factory for creating scoped contexts. +public class BookingCategoryRepository(IDbContextFactory contextFactory) : IBookingCategoryRepository +{ + /// + public async Task> 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); + } + + /// + public async Task 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); + } + + /// + public async Task 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); + } + + /// + public async Task 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; + } + + /// + public async Task 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; + } +} diff --git a/src/Koogle.Infrastructure/Repositories/CashBookEntryRepository.cs b/src/Koogle.Infrastructure/Repositories/CashBookEntryRepository.cs new file mode 100644 index 0000000..04cc603 --- /dev/null +++ b/src/Koogle.Infrastructure/Repositories/CashBookEntryRepository.cs @@ -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; + +/// +/// Repository implementation for managing cash book entry entities. +/// +/// The database context factory for creating scoped contexts. +public class CashBookEntryRepository(IDbContextFactory contextFactory) : ICashBookEntryRepository +{ + /// + public async Task> GetByClubIdAsync(Guid clubId, DateTime? from, DateTime? to, CancellationToken ct = default) + { + await using var context = await contextFactory.CreateDbContextAsync(ct); + IQueryable 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); + } + + /// + public async Task 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); + } + + /// + public async Task 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; + } + + /// + public async Task 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; + } + + /// + public async Task> 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); + } + + /// + public async Task 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); + } +}