Add Trigger-Engine (Phase H0)
- ITriggerRepository interface + TriggerRepository implementation - ITriggerService interface + TriggerService implementation - TriggerDto, ExpenseTriggerLinkDto, FireTriggerDto - FireTriggerAsync creates PersonExpenses with multiplier - DI registration for both services - Fix test mock for IClubRepository 🤖 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
29cebcbb81
commit
e8df51d67f
|
|
@ -0,0 +1,76 @@
|
|||
using Koogle.Domain.Enums;
|
||||
|
||||
namespace Koogle.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Data transfer object representing a trigger.
|
||||
/// </summary>
|
||||
public record TriggerDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier of the trigger.
|
||||
/// </summary>
|
||||
public Guid Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the trigger.
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The type of expense trigger.
|
||||
/// </summary>
|
||||
public ExpenseTriggerType ExpenseTriggerType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Expenses linked to this trigger for the current club.
|
||||
/// </summary>
|
||||
public List<ExpenseSummaryDto> LinkedExpenses { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data transfer object for linking/unlinking an expense to a trigger.
|
||||
/// </summary>
|
||||
public record ExpenseTriggerLinkDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The expense identifier.
|
||||
/// </summary>
|
||||
public Guid ExpenseId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The trigger identifier.
|
||||
/// </summary>
|
||||
public Guid TriggerId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data transfer object for firing a trigger.
|
||||
/// </summary>
|
||||
public record FireTriggerDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The trigger type to fire.
|
||||
/// </summary>
|
||||
public ExpenseTriggerType TriggerType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The person receiving the expense(s).
|
||||
/// </summary>
|
||||
public Guid PersonId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The day context for the expense.
|
||||
/// </summary>
|
||||
public Guid DayId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional game context for the expense.
|
||||
/// </summary>
|
||||
public Guid? GameId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier for expense price (e.g., remaining points). Default is 1.
|
||||
/// </summary>
|
||||
public int Multiplier { get; init; } = 1;
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ namespace Koogle.Application
|
|||
services.AddScoped<IPersonExpenseService, PersonExpenseService>();
|
||||
services.AddScoped<IDashboardService, DashboardService>();
|
||||
services.AddScoped<IEmailService, StubEmailService>();
|
||||
services.AddScoped<ITriggerService, TriggerService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
using Koogle.Application.DTOs;
|
||||
using Koogle.Domain.Enums;
|
||||
|
||||
namespace Koogle.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service interface for managing triggers and firing automatic expense assignments.
|
||||
/// </summary>
|
||||
public interface ITriggerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all available trigger types with their associated expenses for the current club.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A list of trigger DTOs with their linked expenses.</returns>
|
||||
Task<List<TriggerDto>> GetAllTriggersAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all expenses linked to a specific trigger type for the current club.
|
||||
/// </summary>
|
||||
/// <param name="type">The expense trigger type.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A list of expenses linked to the trigger type.</returns>
|
||||
Task<List<ExpenseDto>> GetExpensesForTriggerAsync(ExpenseTriggerType type, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a trigger type has any linked expenses for the current club.
|
||||
/// </summary>
|
||||
/// <param name="type">The expense trigger type.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>True if the trigger has linked expenses; otherwise, false.</returns>
|
||||
Task<bool> HasExpensesForTriggerAsync(ExpenseTriggerType type, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Fires a trigger, creating PersonExpenses for all linked expenses.
|
||||
/// </summary>
|
||||
/// <param name="type">The trigger type (e.g., ExpensePoint, Circle, NinePins).</param>
|
||||
/// <param name="personId">The person receiving the expense(s).</param>
|
||||
/// <param name="dayId">The day context for the expense.</param>
|
||||
/// <param name="gameId">Optional game context for the expense.</param>
|
||||
/// <param name="multiplier">Multiplier for expense price (e.g., remaining points in Scheiss-Spiel). Default is 1.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A list of created PersonExpense DTOs.</returns>
|
||||
Task<List<PersonExpenseDto>> FireTriggerAsync(
|
||||
ExpenseTriggerType type,
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid? gameId = null,
|
||||
int multiplier = 1,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Links an expense to a trigger for the current club.
|
||||
/// </summary>
|
||||
/// <param name="expenseId">The expense to link.</param>
|
||||
/// <param name="triggerId">The trigger to link to.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task LinkExpenseToTriggerAsync(Guid expenseId, Guid triggerId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Unlinks an expense from a trigger for the current club.
|
||||
/// </summary>
|
||||
/// <param name="expenseId">The expense to unlink.</param>
|
||||
/// <param name="triggerId">The trigger to unlink from.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>True if unlinked successfully; otherwise, false.</returns>
|
||||
Task<bool> UnlinkExpenseFromTriggerAsync(Guid expenseId, Guid triggerId, CancellationToken ct = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
using AutoMapper;
|
||||
using Koogle.Application.DTOs;
|
||||
using Koogle.Application.Interfaces;
|
||||
using Koogle.Domain.Entities;
|
||||
using Koogle.Domain.Enums;
|
||||
using Koogle.Domain.Interfaces;
|
||||
using KoogleApp.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Koogle.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing triggers and firing automatic expense assignments.
|
||||
/// </summary>
|
||||
public class TriggerService : ITriggerService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _contextFactory;
|
||||
private readonly ITriggerRepository _triggerRepository;
|
||||
private readonly ICurrentClubContext _clubContext;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TriggerService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="contextFactory">The database context factory.</param>
|
||||
/// <param name="triggerRepository">The trigger repository.</param>
|
||||
/// <param name="clubContext">The current club context.</param>
|
||||
/// <param name="mapper">The AutoMapper instance.</param>
|
||||
public TriggerService(
|
||||
IDbContextFactory<AppDbContext> contextFactory,
|
||||
ITriggerRepository triggerRepository,
|
||||
ICurrentClubContext clubContext,
|
||||
IMapper mapper)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
_triggerRepository = triggerRepository;
|
||||
_clubContext = clubContext;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<TriggerDto>> GetAllTriggersAsync(CancellationToken ct = default)
|
||||
{
|
||||
var triggers = await _triggerRepository.GetAllAsync(ct);
|
||||
var expenseTriggers = await _triggerRepository.GetExpenseTriggersByClubIdAsync(_clubContext.ClubId, ct);
|
||||
|
||||
var result = triggers.Select(t => new TriggerDto
|
||||
{
|
||||
Id = t.Id,
|
||||
Name = t.Name,
|
||||
ExpenseTriggerType = t.ExpenseTriggerType,
|
||||
LinkedExpenses = expenseTriggers
|
||||
.Where(et => et.TriggerId == t.Id && !et.Expense.IsDeleted)
|
||||
.Select(et => new ExpenseSummaryDto
|
||||
{
|
||||
Id = et.Expense.Id,
|
||||
Name = et.Expense.Name,
|
||||
Price = et.Expense.Price,
|
||||
IsOneClick = et.Expense.IsOneClick
|
||||
})
|
||||
.ToList()
|
||||
}).ToList();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<ExpenseDto>> GetExpensesForTriggerAsync(ExpenseTriggerType type, CancellationToken ct = default)
|
||||
{
|
||||
var expenses = await _triggerRepository.GetExpensesByTriggerTypeAsync(_clubContext.ClubId, type, ct);
|
||||
return _mapper.Map<List<ExpenseDto>>(expenses);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> HasExpensesForTriggerAsync(ExpenseTriggerType type, CancellationToken ct = default)
|
||||
{
|
||||
var expenses = await _triggerRepository.GetExpensesByTriggerTypeAsync(_clubContext.ClubId, type, ct);
|
||||
return expenses.Count > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<PersonExpenseDto>> FireTriggerAsync(
|
||||
ExpenseTriggerType type,
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid? gameId = null,
|
||||
int multiplier = 1,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// Get all expenses linked to this trigger type for the current club
|
||||
var expenses = await _triggerRepository.GetExpensesByTriggerTypeAsync(_clubContext.ClubId, type, ct);
|
||||
|
||||
if (expenses.Count == 0)
|
||||
return [];
|
||||
|
||||
// Validate day exists and is not closed
|
||||
var day = await context.Days
|
||||
.Where(d => d.Id == dayId && d.ClubId == _clubContext.ClubId && !d.IsDeleted)
|
||||
.FirstOrDefaultAsync(ct)
|
||||
?? throw new InvalidOperationException($"Day with id {dayId} not found.");
|
||||
|
||||
if (day.Status == DayStatus.Closed)
|
||||
throw new InvalidOperationException("Cannot add expenses to a closed day.");
|
||||
|
||||
// Validate person exists
|
||||
var person = await context.Persons
|
||||
.Where(p => p.Id == personId && p.ClubId == _clubContext.ClubId && !p.IsDeleted)
|
||||
.FirstOrDefaultAsync(ct)
|
||||
?? throw new InvalidOperationException($"Person with id {personId} not found.");
|
||||
|
||||
var createdExpenses = new List<PersonExpense>();
|
||||
|
||||
foreach (var expense in expenses)
|
||||
{
|
||||
// Calculate price with multiplier
|
||||
var price = expense.Price * multiplier;
|
||||
|
||||
var personExpense = new PersonExpense
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PersonId = personId,
|
||||
ExpenseId = expense.Id,
|
||||
DayId = dayId,
|
||||
GameId = gameId,
|
||||
ClubId = _clubContext.ClubId,
|
||||
Name = expense.Name,
|
||||
Price = price,
|
||||
ExpenseType = expense.ExpenseType,
|
||||
PersonExpenseStatus = PersonExpenseStatus.Open,
|
||||
AssignedById = Guid.Empty, // TODO: Get from ICurrentUserService when available
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
IsDeleted = false
|
||||
};
|
||||
|
||||
context.PersonExpenses.Add(personExpense);
|
||||
createdExpenses.Add(personExpense);
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
|
||||
// Reload with navigation properties
|
||||
var result = new List<PersonExpenseDto>();
|
||||
foreach (var pe in createdExpenses)
|
||||
{
|
||||
var loaded = await context.PersonExpenses
|
||||
.Where(p => p.Id == pe.Id)
|
||||
.Include(p => p.Person)
|
||||
.Include(p => p.Day)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (loaded is not null)
|
||||
{
|
||||
result.Add(MapToDto(loaded));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task LinkExpenseToTriggerAsync(Guid expenseId, Guid triggerId, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// Validate expense exists for current club
|
||||
var expense = await context.Expenses
|
||||
.Where(e => e.Id == expenseId && e.ClubId == _clubContext.ClubId && !e.IsDeleted)
|
||||
.FirstOrDefaultAsync(ct)
|
||||
?? throw new InvalidOperationException($"Expense with id {expenseId} not found.");
|
||||
|
||||
// Validate trigger exists
|
||||
var trigger = await _triggerRepository.GetByIdAsync(triggerId, ct)
|
||||
?? throw new InvalidOperationException($"Trigger with id {triggerId} not found.");
|
||||
|
||||
// Check if mapping already exists
|
||||
var existing = await context.ExpenseTriggers
|
||||
.FirstOrDefaultAsync(et =>
|
||||
et.ClubId == _clubContext.ClubId &&
|
||||
et.ExpenseId == expenseId &&
|
||||
et.TriggerId == triggerId, ct);
|
||||
|
||||
if (existing is not null)
|
||||
return; // Already linked
|
||||
|
||||
var expenseTrigger = new ExpenseTrigger
|
||||
{
|
||||
ClubId = _clubContext.ClubId,
|
||||
ExpenseId = expenseId,
|
||||
TriggerId = triggerId,
|
||||
AssignedAt = DateTime.UtcNow,
|
||||
AssignedById = Guid.Empty // TODO: Get from ICurrentUserService when available
|
||||
};
|
||||
|
||||
await _triggerRepository.AddExpenseTriggerAsync(expenseTrigger, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UnlinkExpenseFromTriggerAsync(Guid expenseId, Guid triggerId, CancellationToken ct = default)
|
||||
{
|
||||
return await _triggerRepository.RemoveExpenseTriggerAsync(_clubContext.ClubId, expenseId, triggerId, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a PersonExpense entity to a PersonExpenseDto.
|
||||
/// </summary>
|
||||
private static PersonExpenseDto MapToDto(PersonExpense entity)
|
||||
{
|
||||
return new PersonExpenseDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
PersonId = entity.PersonId,
|
||||
PersonName = entity.Person?.Name ?? string.Empty,
|
||||
ExpenseId = entity.ExpenseId,
|
||||
DayId = entity.DayId,
|
||||
DayPostDate = entity.Day?.PostDate ?? DateTime.MinValue,
|
||||
GameId = entity.GameId,
|
||||
ClubId = entity.ClubId,
|
||||
Name = entity.Name,
|
||||
Price = entity.Price,
|
||||
ExpenseType = entity.ExpenseType,
|
||||
PersonExpenseStatus = entity.PersonExpenseStatus,
|
||||
AssignedById = entity.AssignedById,
|
||||
CreatedAt = entity.CreatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
using Koogle.Domain.Entities;
|
||||
using Koogle.Domain.Enums;
|
||||
|
||||
namespace Koogle.Domain.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for managing trigger entities and expense-trigger relationships.
|
||||
/// </summary>
|
||||
public interface ITriggerRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves all triggers.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A list of all triggers.</returns>
|
||||
Task<List<Trigger>> GetAllAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a trigger by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the trigger.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The trigger if found; otherwise, null.</returns>
|
||||
Task<Trigger?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a trigger by its type.
|
||||
/// </summary>
|
||||
/// <param name="type">The expense trigger type.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The trigger if found; otherwise, null.</returns>
|
||||
Task<Trigger?> GetByTypeAsync(ExpenseTriggerType type, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all expense-trigger mappings for a specific club.
|
||||
/// </summary>
|
||||
/// <param name="clubId">The unique identifier of the club.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A list of expense-trigger mappings for the club.</returns>
|
||||
Task<List<ExpenseTrigger>> GetExpenseTriggersByClubIdAsync(Guid clubId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all expenses linked to a specific trigger type for a club.
|
||||
/// </summary>
|
||||
/// <param name="clubId">The unique identifier of the club.</param>
|
||||
/// <param name="type">The expense trigger type.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A list of expenses linked to the trigger type.</returns>
|
||||
Task<List<Expense>> GetExpensesByTriggerTypeAsync(Guid clubId, ExpenseTriggerType type, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an expense-trigger mapping.
|
||||
/// </summary>
|
||||
/// <param name="expenseTrigger">The expense-trigger mapping to add.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task AddExpenseTriggerAsync(ExpenseTrigger expenseTrigger, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes an expense-trigger mapping.
|
||||
/// </summary>
|
||||
/// <param name="clubId">The club identifier.</param>
|
||||
/// <param name="expenseId">The expense identifier.</param>
|
||||
/// <param name="triggerId">The trigger identifier.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>True if removed successfully; otherwise, false.</returns>
|
||||
Task<bool> RemoveExpenseTriggerAsync(Guid clubId, Guid expenseId, Guid triggerId, CancellationToken ct = default);
|
||||
}
|
||||
|
|
@ -79,6 +79,7 @@ public static class DependencyInjection
|
|||
services.AddScoped<IExpenseRepository, ExpenseRepository>();
|
||||
services.AddScoped<IDayRepository, DayRepository>();
|
||||
services.AddScoped<IPersonExpenseRepository, PersonExpenseRepository>();
|
||||
services.AddScoped<ITriggerRepository, TriggerRepository>();
|
||||
|
||||
// Services
|
||||
//services.AddScoped<IEmailService, StubEmailService>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
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 trigger entities and expense-trigger relationships.
|
||||
/// </summary>
|
||||
/// <param name="contextFactory">The database context factory for creating scoped contexts.</param>
|
||||
public class TriggerRepository(IDbContextFactory<AppDbContext> contextFactory) : ITriggerRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<List<Trigger>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.Triggers
|
||||
.Where(t => !t.IsDeleted)
|
||||
.OrderBy(t => t.Name)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Trigger?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.Triggers
|
||||
.FirstOrDefaultAsync(t => t.Id == id && !t.IsDeleted, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Trigger?> GetByTypeAsync(ExpenseTriggerType type, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.Triggers
|
||||
.FirstOrDefaultAsync(t => t.ExpenseTriggerType == type && !t.IsDeleted, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<ExpenseTrigger>> GetExpenseTriggersByClubIdAsync(Guid clubId, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.ExpenseTriggers
|
||||
.Where(et => et.ClubId == clubId)
|
||||
.Include(et => et.Expense)
|
||||
.Include(et => et.Trigger)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<Expense>> GetExpensesByTriggerTypeAsync(Guid clubId, ExpenseTriggerType type, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.ExpenseTriggers
|
||||
.Where(et => et.ClubId == clubId && et.Trigger.ExpenseTriggerType == type)
|
||||
.Select(et => et.Expense)
|
||||
.Where(e => !e.IsDeleted)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddExpenseTriggerAsync(ExpenseTrigger expenseTrigger, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
expenseTrigger.AssignedAt = DateTime.UtcNow;
|
||||
await context.ExpenseTriggers.AddAsync(expenseTrigger, ct);
|
||||
await context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> RemoveExpenseTriggerAsync(Guid clubId, Guid expenseId, Guid triggerId, CancellationToken ct = default)
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(ct);
|
||||
var entity = await context.ExpenseTriggers
|
||||
.FirstOrDefaultAsync(et => et.ClubId == clubId && et.ExpenseId == expenseId && et.TriggerId == triggerId, ct);
|
||||
|
||||
if (entity is null)
|
||||
return false;
|
||||
|
||||
context.ExpenseTriggers.Remove(entity);
|
||||
await context.SaveChangesAsync(ct);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -408,4 +408,15 @@ internal class ClubRepository : Koogle.Domain.Interfaces.IClubRepository
|
|||
await _context.SaveChangesAsync(ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<Club?> GetByNameAsync(string clubName, Guid? excludeGuid, CancellationToken ct = default)
|
||||
{
|
||||
var query = _context.Clubs
|
||||
.Where(c => !c.IsDeleted && c.Name.ToLower() == clubName.ToLower());
|
||||
if (excludeGuid.HasValue)
|
||||
{
|
||||
query = query.Where(c => c.Id != excludeGuid.Value);
|
||||
}
|
||||
return await query.FirstOrDefaultAsync(ct);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue