From 29cebcbb815eb781be2b907bd7e353bae499ee66 Mon Sep 17 00:00:00 2001 From: beo3000 Date: Fri, 26 Dec 2025 13:39:23 +0100 Subject: [PATCH] ensure unique names for club,person,expense --- src/Koogle.Application/Services/ClubService.cs | 13 +++++++++++++ .../Services/ExpenseService.cs | 13 +++++++++++++ .../Services/PersonService.cs | 4 ++-- src/Koogle.Domain/Interfaces/IClubRepository.cs | 9 +++++++++ .../Interfaces/IExpenseRepository.cs | 10 ++++++++++ .../Interfaces/IPersonRepository.cs | 7 ++++--- .../Repositories/ClubRepository.cs | 14 ++++++++++++++ .../Repositories/ExpenseRepository.cs | 14 ++++++++++++++ .../Repositories/PersonRepository.cs | 17 ++++++++++++++--- 9 files changed, 93 insertions(+), 8 deletions(-) diff --git a/src/Koogle.Application/Services/ClubService.cs b/src/Koogle.Application/Services/ClubService.cs index c38f8c2..20435be 100644 --- a/src/Koogle.Application/Services/ClubService.cs +++ b/src/Koogle.Application/Services/ClubService.cs @@ -3,6 +3,7 @@ using Koogle.Application.DTOs; using Koogle.Application.Interfaces; using Koogle.Domain.Entities; using Koogle.Domain.Interfaces; +using Koogle.Infrastructure.Repositories; using KoogleApp.Data; using Microsoft.EntityFrameworkCore; using System.Security.Cryptography; @@ -48,6 +49,12 @@ public class ClubService : IClubService /// public async Task CreateAsync(CreateClubDto dto, CancellationToken ct = default) { + var existingClub = await _clubRepository.GetByNameAsync(dto.Name, null, ct); + if (existingClub != null) + { + throw new InvalidOperationException($"Club with name '{dto.Name}' already exists"); + } + var entity = new Club { Id = Guid.NewGuid(), @@ -67,6 +74,12 @@ public class ClubService : IClubService var existing = await _clubRepository.GetByIdAsync(dto.Id, ct) ?? throw new InvalidOperationException($"Club with id {dto.Id} not found."); + var existingClub = await _clubRepository.GetByNameAsync(dto.Name, dto.Id, ct); + if (existingClub != null) + { + throw new InvalidOperationException($"Club with name '{dto.Name}' already exists"); + } + existing.Name = dto.Name; existing.ExpenseCalculation = dto.ExpenseCalculation; existing.ModifiedAt = DateTime.UtcNow; diff --git a/src/Koogle.Application/Services/ExpenseService.cs b/src/Koogle.Application/Services/ExpenseService.cs index d2bb12a..5aa3e74 100644 --- a/src/Koogle.Application/Services/ExpenseService.cs +++ b/src/Koogle.Application/Services/ExpenseService.cs @@ -3,6 +3,7 @@ using Koogle.Application.DTOs; using Koogle.Application.Interfaces; using Koogle.Domain.Entities; using Koogle.Domain.Interfaces; +using Koogle.Infrastructure.Repositories; namespace Koogle.Application.Services; @@ -51,6 +52,12 @@ public class ExpenseService : IExpenseService /// public async Task CreateAsync(CreateExpenseDto dto, CancellationToken ct = default) { + var existingExpense = await _expenseRepository.GetByNameAsync(_clubContext.ClubId, dto.Name, null, ct); + if (existingExpense != null) + { + throw new InvalidOperationException($"Expense with name '{dto.Name}' already exists"); + } + var entity = new Expense { Id = Guid.NewGuid(), @@ -79,6 +86,12 @@ public class ExpenseService : IExpenseService if (existing.ClubId != _clubContext.ClubId) throw new InvalidOperationException("Expense does not belong to current club."); + var existingExpense = await _expenseRepository.GetByNameAsync(_clubContext.ClubId, dto.Name, dto.Id, ct); + if (existingExpense != null) + { + throw new InvalidOperationException($"Expense with name '{dto.Name}' already exists"); + } + existing.Name = dto.Name; existing.Description = dto.Description; existing.Price = dto.Price; diff --git a/src/Koogle.Application/Services/PersonService.cs b/src/Koogle.Application/Services/PersonService.cs index 8040b0b..1e4d202 100644 --- a/src/Koogle.Application/Services/PersonService.cs +++ b/src/Koogle.Application/Services/PersonService.cs @@ -52,7 +52,7 @@ public class PersonService : IPersonService /// public async Task CreateAsync(CreatePersonDto dto, CancellationToken ct = default) { - var existingPerson = await _personRepository.GetByNameAsync(_clubContext.ClubId, dto.Name, ct); + var existingPerson = await _personRepository.GetByNameAsync(_clubContext.ClubId, dto.Name, null, ct); if (existingPerson != null) { throw new InvalidOperationException($"Person with name '{dto.Name}' already exists"); @@ -81,7 +81,7 @@ public class PersonService : IPersonService if (existing.ClubId != _clubContext.ClubId) throw new InvalidOperationException("Person does not belong to current club."); - var existingPerson = await _personRepository.GetByNameAsync(_clubContext.ClubId, dto.Name, ct); + var existingPerson = await _personRepository.GetByNameAsync(_clubContext.ClubId, dto.Name, dto.Id, ct); if (existingPerson != null) { throw new InvalidOperationException($"Person with name '{dto.Name}' already exists"); diff --git a/src/Koogle.Domain/Interfaces/IClubRepository.cs b/src/Koogle.Domain/Interfaces/IClubRepository.cs index 6133f45..4b2a4f4 100644 --- a/src/Koogle.Domain/Interfaces/IClubRepository.cs +++ b/src/Koogle.Domain/Interfaces/IClubRepository.cs @@ -45,4 +45,13 @@ public interface IClubRepository /// Cancellation token. /// True if deleted successfully; otherwise, false. Task DeleteAsync(Guid id, CancellationToken ct = default); + + /// + /// Get a club by its name within a specific context. + /// + /// Name to search for + /// A unique ID to exclude from search + /// Cancellation token. + /// + Task GetByNameAsync(string clubName, Guid? excludeGuid, CancellationToken ct = default); } diff --git a/src/Koogle.Domain/Interfaces/IExpenseRepository.cs b/src/Koogle.Domain/Interfaces/IExpenseRepository.cs index 40a9d6a..c1b0c9b 100644 --- a/src/Koogle.Domain/Interfaces/IExpenseRepository.cs +++ b/src/Koogle.Domain/Interfaces/IExpenseRepository.cs @@ -46,4 +46,14 @@ public interface IExpenseRepository /// Cancellation token. /// True if deleted successfully; otherwise, false. Task DeleteAsync(Guid id, CancellationToken ct = default); + + /// + /// Read an expense by its name within a specific club context. + /// + /// + /// + /// Guid of an expense, that should be excluded + /// + /// The expense with the name or null + Task GetByNameAsync(Guid clubId, string expenseName, Guid? excludeGuid, CancellationToken ct = default); } diff --git a/src/Koogle.Domain/Interfaces/IPersonRepository.cs b/src/Koogle.Domain/Interfaces/IPersonRepository.cs index 957b466..11da9fe 100644 --- a/src/Koogle.Domain/Interfaces/IPersonRepository.cs +++ b/src/Koogle.Domain/Interfaces/IPersonRepository.cs @@ -50,9 +50,10 @@ public interface IPersonRepository /// /// Read a person by its name within a specific club context. /// - /// - /// + /// + /// + /// Guid of a person, that should be excluded /// /// The person with the name or null - Task GetByNameAsync(Guid clubContextClubId, string dtoName, CancellationToken ct); + Task GetByNameAsync(Guid clubId, string personName, Guid? excludeGuid, CancellationToken ct = default); } diff --git a/src/Koogle.Infrastructure/Repositories/ClubRepository.cs b/src/Koogle.Infrastructure/Repositories/ClubRepository.cs index 4c84e4c..b80d549 100644 --- a/src/Koogle.Infrastructure/Repositories/ClubRepository.cs +++ b/src/Koogle.Infrastructure/Repositories/ClubRepository.cs @@ -61,4 +61,18 @@ public class ClubRepository(IDbContextFactory contextFactory) : IC await context.SaveChangesAsync(ct); return true; } + + /// + public async Task GetByNameAsync(string clubName, Guid? excludeGuid, CancellationToken ct = default) + { + await using var context = await contextFactory.CreateDbContextAsync(ct); + var query = context.Clubs + .Where(c => c.Name.ToLower() == clubName.ToLower() && !c.IsDeleted); + if (excludeGuid.HasValue) + { + query = query.Where(c => c.Id != excludeGuid.Value); + } + var entity = await query.FirstOrDefaultAsync(ct); + return entity; + } } diff --git a/src/Koogle.Infrastructure/Repositories/ExpenseRepository.cs b/src/Koogle.Infrastructure/Repositories/ExpenseRepository.cs index 0d18ea7..fb6061b 100644 --- a/src/Koogle.Infrastructure/Repositories/ExpenseRepository.cs +++ b/src/Koogle.Infrastructure/Repositories/ExpenseRepository.cs @@ -63,4 +63,18 @@ public class ExpenseRepository(IDbContextFactory contextFactory) : await context.SaveChangesAsync(ct); return true; } + + /// + public async Task GetByNameAsync(Guid clubId, string expenseName, Guid? excludeGuid, CancellationToken ct = default) + { + await using var context = await contextFactory.CreateDbContextAsync(ct); + var query = context.Expenses + .Where(e => e.ClubId == clubId && !e.IsDeleted && e.Name.ToLower() == expenseName.ToLower()); + if (excludeGuid.HasValue) + { + query = query.Where(e => e.Id != excludeGuid.Value); + } + var entity = await query.FirstOrDefaultAsync(ct); + return entity; + } } diff --git a/src/Koogle.Infrastructure/Repositories/PersonRepository.cs b/src/Koogle.Infrastructure/Repositories/PersonRepository.cs index cfb60a0..b8e6eb9 100644 --- a/src/Koogle.Infrastructure/Repositories/PersonRepository.cs +++ b/src/Koogle.Infrastructure/Repositories/PersonRepository.cs @@ -65,11 +65,22 @@ public class PersonRepository(IDbContextFactory contextFactory) : } /// - public async Task GetByNameAsync(Guid clubContextClubId, string dtoName, CancellationToken ct) + public async Task GetByNameAsync(Guid clubId, string personName, Guid? excludeGuid, CancellationToken ct = default) { await using var context = await contextFactory.CreateDbContextAsync(ct); - var entity = await context.Persons - .FirstOrDefaultAsync(p => p.ClubId == clubContextClubId && p.Name == dtoName && !p.IsDeleted, ct); + Person? entity; + if (excludeGuid != null) + { + entity = await context.Persons + .FirstOrDefaultAsync( + p => p.ClubId == clubId && p.Name.ToLower() == personName.ToLower() && !p.IsDeleted && p.Id != excludeGuid, ct); + } else + { + entity = await context.Persons + .FirstOrDefaultAsync( + p => p.ClubId == clubId && p.Name.ToLower() == personName.ToLower() && !p.IsDeleted, ct); + } + return entity; } }