ensure unique names for club,person,expense

This commit is contained in:
beo3000 2025-12-26 13:39:23 +01:00
parent 7a96cf0cc4
commit 29cebcbb81
9 changed files with 93 additions and 8 deletions

View File

@ -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
/// <inheritdoc />
public async Task<ClubDto> 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;

View File

@ -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
/// <inheritdoc />
public async Task<ExpenseDto> 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;

View File

@ -52,7 +52,7 @@ public class PersonService : IPersonService
/// <inheritdoc />
public async Task<PersonDto> 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");

View File

@ -45,4 +45,13 @@ public interface IClubRepository
/// <param name="ct">Cancellation token.</param>
/// <returns>True if deleted successfully; otherwise, false.</returns>
Task<bool> DeleteAsync(Guid id, CancellationToken ct = default);
/// <summary>
/// Get a club by its name within a specific context.
/// </summary>
/// <param name="clubName">Name to search for</param>
/// <param name="excludeGuid">A unique ID to exclude from search</param>
/// <param name="ct">Cancellation token.</param>
/// <returns></returns>
Task<Club?> GetByNameAsync(string clubName, Guid? excludeGuid, CancellationToken ct = default);
}

View File

@ -46,4 +46,14 @@ public interface IExpenseRepository
/// <param name="ct">Cancellation token.</param>
/// <returns>True if deleted successfully; otherwise, false.</returns>
Task<bool> DeleteAsync(Guid id, CancellationToken ct = default);
/// <summary>
/// Read an expense by its name within a specific club context.
/// </summary>
/// <param name="clubId"></param>
/// <param name="expenseName"></param>
/// <param name="excludeGuid">Guid of an expense, that should be excluded</param>
/// <param name="ct"></param>
/// <returns>The expense with the name or null</returns>
Task<Expense?> GetByNameAsync(Guid clubId, string expenseName, Guid? excludeGuid, CancellationToken ct = default);
}

View File

@ -50,9 +50,10 @@ public interface IPersonRepository
/// <summary>
/// Read a person by its name within a specific club context.
/// </summary>
/// <param name="clubContextClubId"></param>
/// <param name="dtoName"></param>
/// <param name="clubId"></param>
/// <param name="personName"></param>
/// <param name="excludeGuid">Guid of a person, that should be excluded</param>
/// <param name="ct"></param>
/// <returns>The person with the name or null</returns>
Task<Person?> GetByNameAsync(Guid clubContextClubId, string dtoName, CancellationToken ct);
Task<Person?> GetByNameAsync(Guid clubId, string personName, Guid? excludeGuid, CancellationToken ct = default);
}

View File

@ -61,4 +61,18 @@ public class ClubRepository(IDbContextFactory<AppDbContext> contextFactory) : IC
await context.SaveChangesAsync(ct);
return true;
}
/// <inheritdoc />
public async Task<Club?> 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;
}
}

View File

@ -63,4 +63,18 @@ public class ExpenseRepository(IDbContextFactory<AppDbContext> contextFactory) :
await context.SaveChangesAsync(ct);
return true;
}
/// <inheritdoc />
public async Task<Expense?> 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;
}
}

View File

@ -65,11 +65,22 @@ public class PersonRepository(IDbContextFactory<AppDbContext> contextFactory) :
}
/// <inheritdoc />
public async Task<Person?> GetByNameAsync(Guid clubContextClubId, string dtoName, CancellationToken ct)
public async Task<Person?> 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;
}
}