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.Application.Interfaces;
using Koogle.Domain.Entities; using Koogle.Domain.Entities;
using Koogle.Domain.Interfaces; using Koogle.Domain.Interfaces;
using Koogle.Infrastructure.Repositories;
using KoogleApp.Data; using KoogleApp.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -48,6 +49,12 @@ public class ClubService : IClubService
/// <inheritdoc /> /// <inheritdoc />
public async Task<ClubDto> CreateAsync(CreateClubDto dto, CancellationToken ct = default) 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 var entity = new Club
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
@ -67,6 +74,12 @@ public class ClubService : IClubService
var existing = await _clubRepository.GetByIdAsync(dto.Id, ct) var existing = await _clubRepository.GetByIdAsync(dto.Id, ct)
?? throw new InvalidOperationException($"Club with id {dto.Id} not found."); ?? 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.Name = dto.Name;
existing.ExpenseCalculation = dto.ExpenseCalculation; existing.ExpenseCalculation = dto.ExpenseCalculation;
existing.ModifiedAt = DateTime.UtcNow; existing.ModifiedAt = DateTime.UtcNow;

View File

@ -3,6 +3,7 @@ using Koogle.Application.DTOs;
using Koogle.Application.Interfaces; using Koogle.Application.Interfaces;
using Koogle.Domain.Entities; using Koogle.Domain.Entities;
using Koogle.Domain.Interfaces; using Koogle.Domain.Interfaces;
using Koogle.Infrastructure.Repositories;
namespace Koogle.Application.Services; namespace Koogle.Application.Services;
@ -51,6 +52,12 @@ public class ExpenseService : IExpenseService
/// <inheritdoc /> /// <inheritdoc />
public async Task<ExpenseDto> CreateAsync(CreateExpenseDto dto, CancellationToken ct = default) 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 var entity = new Expense
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
@ -79,6 +86,12 @@ public class ExpenseService : IExpenseService
if (existing.ClubId != _clubContext.ClubId) if (existing.ClubId != _clubContext.ClubId)
throw new InvalidOperationException("Expense does not belong to current club."); 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.Name = dto.Name;
existing.Description = dto.Description; existing.Description = dto.Description;
existing.Price = dto.Price; existing.Price = dto.Price;

View File

@ -52,7 +52,7 @@ public class PersonService : IPersonService
/// <inheritdoc /> /// <inheritdoc />
public async Task<PersonDto> CreateAsync(CreatePersonDto dto, CancellationToken ct = default) 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) if (existingPerson != null)
{ {
throw new InvalidOperationException($"Person with name '{dto.Name}' already exists"); throw new InvalidOperationException($"Person with name '{dto.Name}' already exists");
@ -81,7 +81,7 @@ public class PersonService : IPersonService
if (existing.ClubId != _clubContext.ClubId) if (existing.ClubId != _clubContext.ClubId)
throw new InvalidOperationException("Person does not belong to current club."); 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) if (existingPerson != null)
{ {
throw new InvalidOperationException($"Person with name '{dto.Name}' already exists"); 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> /// <param name="ct">Cancellation token.</param>
/// <returns>True if deleted successfully; otherwise, false.</returns> /// <returns>True if deleted successfully; otherwise, false.</returns>
Task<bool> DeleteAsync(Guid id, CancellationToken ct = default); 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> /// <param name="ct">Cancellation token.</param>
/// <returns>True if deleted successfully; otherwise, false.</returns> /// <returns>True if deleted successfully; otherwise, false.</returns>
Task<bool> DeleteAsync(Guid id, CancellationToken ct = default); 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> /// <summary>
/// Read a person by its name within a specific club context. /// Read a person by its name within a specific club context.
/// </summary> /// </summary>
/// <param name="clubContextClubId"></param> /// <param name="clubId"></param>
/// <param name="dtoName"></param> /// <param name="personName"></param>
/// <param name="excludeGuid">Guid of a person, that should be excluded</param>
/// <param name="ct"></param> /// <param name="ct"></param>
/// <returns>The person with the name or null</returns> /// <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); await context.SaveChangesAsync(ct);
return true; 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); await context.SaveChangesAsync(ct);
return true; 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 /> /// <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); await using var context = await contextFactory.CreateDbContextAsync(ct);
var entity = await context.Persons Person? entity;
.FirstOrDefaultAsync(p => p.ClubId == clubContextClubId && p.Name == dtoName && !p.IsDeleted, ct); 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; return entity;
} }
} }