Sube carpetas API, API.Tests, Application y Domain del Backend
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a backup step fails in an expected, handled way that callers must catch and log instead of letting it crash the host.
|
||||
/// </summary>
|
||||
public sealed class BackupExecutionException : Exception
|
||||
{
|
||||
public BackupExecutionException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public BackupExecutionException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata describing a single stored backup dump: its stored name and creation timestamp.
|
||||
/// </summary>
|
||||
public sealed record BackupFile(string Name, DateTimeOffset Timestamp);
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Outcome of a single IBackupOperationsService call, mapped explicitly by controllers to an HTTP status code.
|
||||
/// </summary>
|
||||
public enum BackupOperationOutcome
|
||||
{
|
||||
Completed,
|
||||
Busy,
|
||||
NotFound,
|
||||
Failed,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Application.DTOs.Backup.Response;
|
||||
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Result of a create, delete, or restore attempt, with Record populated only when Outcome is Completed.
|
||||
/// </summary>
|
||||
public sealed record BackupOperationResult(
|
||||
BackupOperationOutcome Outcome, BackupRecordResponse? Record, string? Message);
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Non-secret backup configuration bound from the Backup configuration section.
|
||||
/// </summary>
|
||||
public sealed class BackupOptions
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public int IntervalHours { get; set; } = 24;
|
||||
public int RetentionCount { get; set; } = 7;
|
||||
public string StorageTarget { get; set; } = BackupStorageTargets.Local;
|
||||
public string LocalStoragePath { get; set; } = "backups";
|
||||
public string PgDumpPath { get; set; } = "pg_dump";
|
||||
public string PsqlPath { get; set; } = "psql";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Named BackupOptions.StorageTarget values, where any value other than Supabase resolves to the local storage backend so a configuration typo fails safe.
|
||||
/// </summary>
|
||||
public static class BackupStorageTargets
|
||||
{
|
||||
public const string Local = "Local";
|
||||
public const string Supabase = "Supabase";
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Durable, queryable per-backup record store that is the source of truth for the admin backup listing.
|
||||
/// </summary>
|
||||
public interface IBackupCatalog
|
||||
{
|
||||
Task<BackupRecord> AddAsync(BackupRecord record, CancellationToken ct = default);
|
||||
|
||||
Task<BackupRecord?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<BackupRecord>> ListNewestFirstAsync(CancellationToken ct = default);
|
||||
|
||||
Task RemoveAsync(Guid id, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Domain.Entities.Models;
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// The single shared write path for backup operations, serialized by BackupOperationLock so a concurrent attempt returns Busy instead of running alongside another.
|
||||
/// </summary>
|
||||
public interface IBackupOperationsService
|
||||
{
|
||||
Task<IReadOnlyList<BackupRecord>> ListNewestFirstAsync(CancellationToken ct = default);
|
||||
|
||||
Task<BackupOperationResult> CreateBackupAsync(BackupOrigin origin, CancellationToken ct = default);
|
||||
|
||||
Task<BackupOperationResult> DeleteBackupAsync(Guid id, CancellationToken ct = default);
|
||||
|
||||
Task<BackupOperationResult> RestoreBackupAsync(Guid id, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Pure keep-last-N pruning decision over an existing list of backup files; implementations must not perform I/O.
|
||||
/// </summary>
|
||||
public interface IBackupRetentionPolicy
|
||||
{
|
||||
IReadOnlyList<BackupFile> SelectForDeletion(IReadOnlyList<BackupFile> existing, int retainCount);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Persists, lists, and deletes backup dumps in durable off-host storage, whether cloud object storage or a local or mounted directory.
|
||||
/// </summary>
|
||||
public interface IBackupStorage
|
||||
{
|
||||
Task StoreAsync(string name, Stream content, CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<BackupFile>> ListAsync(CancellationToken ct = default);
|
||||
|
||||
Task DeleteAsync(string name, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a readable stream over the stored backup named name, for the restore flow to copy into a local temp file before invoking IDatabaseRestoreService.
|
||||
/// </summary>
|
||||
Task<Stream> OpenReadAsync(string name, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Produces a single database dump without ever letting an underlying failure crash the caller.
|
||||
/// </summary>
|
||||
public interface IDatabaseBackupService
|
||||
{
|
||||
Task<Stream> CreateDumpAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Restores the database from a local plain-SQL dump file.
|
||||
/// </summary>
|
||||
public interface IDatabaseRestoreService
|
||||
{
|
||||
/// <summary>
|
||||
/// Restores from the plain-SQL dump at dumpFilePath.
|
||||
/// </summary>
|
||||
Task RestoreAsync(string dumpFilePath, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide maintenance-mode flag registered as a singleton, in-memory only.
|
||||
/// </summary>
|
||||
public interface IMaintenanceModeState
|
||||
{
|
||||
bool IsActive { get; }
|
||||
|
||||
string? Reason { get; }
|
||||
|
||||
DateTimeOffset? EnteredAtUtc { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Enters maintenance mode with reason.
|
||||
/// </summary>
|
||||
void Enter(string reason);
|
||||
|
||||
/// <summary>
|
||||
/// Exits maintenance mode and is safe to call even when not currently active.
|
||||
/// </summary>
|
||||
void Exit();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Thin abstraction over external process execution.
|
||||
/// </summary>
|
||||
public interface IProcessRunner
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs fileName with the given args, passed as a vector so no argument can be reinterpreted as shell syntax by the child process.
|
||||
/// </summary>
|
||||
Task<ProcessResult> RunAsync(
|
||||
string fileName,
|
||||
IReadOnlyList<string> args,
|
||||
IReadOnlyDictionary<string, string>? environmentVariables = null,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Application.Interfaces.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Result of running an external process via IProcessRunner, where callers must not treat StdOut as valid output unless ExitCode is 0.
|
||||
/// </summary>
|
||||
public sealed record ProcessResult(int ExitCode, string StdOut, string StdErr);
|
||||
@@ -0,0 +1,17 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.Scorer.Response;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Mappers;
|
||||
|
||||
public interface IScorerMapper
|
||||
{
|
||||
ScorerByPlayerResponse FromScorerToScorerByPlayerResponse(Scorer scorer);
|
||||
|
||||
ScorerByTeamResponse FromScorerToScorerByTeamResponse(Scorer scorer);
|
||||
|
||||
PaginatedResponse<ScorerByPlayerResponse> FromPaginatedScorerToPaginatedScorerByPlayerResponse(PaginatedResponse<Scorer> paginatedScorers);
|
||||
|
||||
PaginatedResponse<ScorerByTeamResponse> FromPaginatedMatchToPaginatedScorerByTeamResponse(PaginatedResponse<Match> paginatedMatches);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Application.Interfaces.Mappers;
|
||||
|
||||
internal interface ITeamMapper
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for managing AuditLog entries.
|
||||
/// </summary>
|
||||
public interface IAuditLogRepository : IGenericRepository<AuditLog>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IBlogPostRepository : IGenericRepository<BlogPost>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IClubRepository : IGenericRepository<Club>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IDivisionRepository : IGenericRepository<Division>
|
||||
{
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IDivisionTeamRegistrationRepository : IGenericRepository<DivisionTeamRegistration>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using Application.DTOs.Abstract.Request;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generic repository interface for common CRUD operations on entities.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">The entity type, which must inherit from EntityBase.</typeparam>
|
||||
public interface IGenericRepository<TEntity> where TEntity : EntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an entity by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the entity.</param>
|
||||
/// <param name="includes">Optional navigation property expressions to include related data.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation. The task result contains the entity if found, otherwise null.
|
||||
/// </returns>
|
||||
Task<TEntity?> GetByIdAsync(Guid id, IEnumerable<Expression<Func<TEntity, object>>>? includes = null);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a queryable collection of entities for advanced querying scenarios.
|
||||
/// </summary>
|
||||
IQueryable<TEntity> GetQueryable();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all entities of type TEntity.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation. The task result contains a collection of all entities.
|
||||
/// </returns>
|
||||
Task<IEnumerable<TEntity>> GetAllAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Finds entities matching the given predicate.
|
||||
/// </summary>
|
||||
/// <param name="predicate">The filter expression to match entities.</param>
|
||||
/// <param name="includes">Optional navigation property expressions to include related data.</param>
|
||||
/// <param name="filter">Optional pagination and filtering request.</param>
|
||||
/// <param name="asSplitQuery">
|
||||
/// Set true when including 2+ collection navigations in the same query —
|
||||
/// otherwise EF Core joins them into one cartesian-product result set,
|
||||
/// which is both slow and, combined with pagination, can return
|
||||
/// incorrect rows.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation. The task result contains a collection of matching entities.
|
||||
/// </returns>
|
||||
Task<IEnumerable<TEntity>> FindAsync(
|
||||
Expression<Func<TEntity, bool>> predicate,
|
||||
IEnumerable<Expression<Func<TEntity, object>>>? includes = null,
|
||||
PaginatedFilterRequest? filter = null,
|
||||
bool asSplitQuery = false);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new entity to the repository.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to add.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation. The task result contains the added entity.
|
||||
/// </returns>
|
||||
Task<TEntity> AddAsync(TEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a range of entities to the repository.
|
||||
/// </summary>
|
||||
/// <param name="entities">The entities to add.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation. The task result contains the added entities.
|
||||
/// </returns>
|
||||
Task<IEnumerable<TEntity>> AddRangeAsync(IEnumerable<TEntity> entities);
|
||||
|
||||
/// <summary>
|
||||
/// Marks a single entity for deletion without calling SaveChanges.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to remove.</param>
|
||||
void Remove(TEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all entities matching expression directly via EF Core's ExecuteDeleteAsync, a single DB-side statement that bypasses the change tracker entirely.
|
||||
/// </summary>
|
||||
/// <param name="expression">The filter expression to match entities for removal.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation. The task result contains the number of entities deleted.
|
||||
/// </returns>
|
||||
Task<int> RemoveAsync(Expression<Func<TEntity, bool>> expression);
|
||||
|
||||
/// <summary>
|
||||
/// Counts the number of entities matching the given predicate, or all if predicate is null.
|
||||
/// </summary>
|
||||
/// <param name="predicate">The filter expression, optional.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation. The task result contains the count of matching entities.
|
||||
/// </returns>
|
||||
Task<int> CountAsync(Expression<Func<TEntity, bool>>? predicate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any entity matches the given predicate.
|
||||
/// </summary>
|
||||
/// <param name="predicate">The filter expression.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation. The task result is true if any entity matches, otherwise false.
|
||||
/// </returns>
|
||||
Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a range of entities in the repository.
|
||||
/// </summary>
|
||||
/// <param name="entities">The entities to update.</param>
|
||||
/// <returns>A task representing the asynchronous update operation.</returns>
|
||||
Task UpdateRangeAsync(IEnumerable<TEntity> entities);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a single entity in the repository.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to update.</param>
|
||||
/// <returns>A task representing the asynchronous update operation.</returns>
|
||||
Task UpdateAsync(TEntity entity);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for managing Match entities.
|
||||
/// </summary>
|
||||
public interface IMatchRepository : IGenericRepository<Match>
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads a single match by its id or slug with everything the public match detail needs: both teams, the venue, and the match's scorers with their players.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The match's GUID id or its slug.</param>
|
||||
/// <returns>The match with its detail graph loaded, or null if not found.</returns>
|
||||
Task<Match?> GetDetailByIdOrSlugAsync(string idOrSlug);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IMatchSeriesRepository : IGenericRepository<MatchSeries>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IPlayerRepository : IGenericRepository<Player>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IPlayerSanctionRepository : IGenericRepository<PlayerSanction>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IPlayerStatisticRepository : IGenericRepository<PlayerStatistic>
|
||||
{
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IPlayerTeamRegistrationRepository : IGenericRepository<PlayerTeamRegistration>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Application.DTOs.Scorer.Request;
|
||||
using Application.DTOs.Scorer.Response;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IScorerRepository : IGenericRepository<Scorer>
|
||||
{
|
||||
Task<(System.Collections.Generic.IEnumerable<ScorerByPlayerResponse> Items, int TotalCount)> GetPlayerScoresAsync(GetScorerFilteredRequest filter);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface ISeasonRepository : IGenericRepository<Season>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IStageRepository : IGenericRepository<Stage>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IStageTeamMatchRepository : IGenericRepository<StageTeamMatch>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Application.DTOs.Statistics.Response;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only historical-statistics aggregations that group a person's data across every season by their stable PlayerId.
|
||||
/// </summary>
|
||||
public interface IStatisticsRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the player's statistic card with total and average points and games played per season and overall, or null when the player does not exist.
|
||||
/// </summary>
|
||||
Task<PlayerStatisticCardResponse?> GetPlayerCardAsync(Guid playerId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the player's per-season trajectory with team, stats, and sanctions for each season they were registered, or null when the player does not exist.
|
||||
/// </summary>
|
||||
Task<PlayerHistoryResponse?> GetPlayerHistoryAsync(Guid playerId);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for managing TeamPointDeduction entities, the disciplinary point deductions applied to teams in a division.
|
||||
/// </summary>
|
||||
public interface ITeamPointDeductionRepository : IGenericRepository<TeamPointDeduction>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface ITeamRepository : IGenericRepository<Team>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for managing TeamStaff entities, a team's technical staff for a given tournament and season.
|
||||
/// </summary>
|
||||
public interface ITeamStaffRepository : IGenericRepository<TeamStaff>
|
||||
{
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface ITeamTournamentRegistrationRepository : IGenericRepository<TeamTournamentRegistration>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface ITournamentRepository : IGenericRepository<Tournament>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Unit of Work interface for coordinating multiple repositories and saving changes atomically.
|
||||
/// </summary>
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
IBlogPostRepository BlogPostRepository { get; }
|
||||
IClubRepository ClubRepository { get; }
|
||||
ITeamRepository TeamRepository { get; }
|
||||
IVenueRepository VenueRepository { get; }
|
||||
ITournamentRepository TournamentRepository { get; }
|
||||
IStageRepository StageRepository { get; }
|
||||
IPlayerStatisticRepository PlayerStatisticRepository { get; }
|
||||
IPlayerSanctionRepository PlayerSanctionRepository { get; }
|
||||
IPlayerRepository PlayerRepository { get; }
|
||||
IMatchRepository MatchRepository { get; }
|
||||
IMatchSeriesRepository MatchSeriesRepository { get; }
|
||||
IDivisionRepository DivisionRepository { get; }
|
||||
IStageTeamMatchRepository StageTeamMatchRepository { get; }
|
||||
IPlayerTeamRegistrationRepository PlayerTeamRegistrationRepository { get; }
|
||||
ITeamTournamentRegistrationRepository TeamTournamentRegistrationRepository { get; }
|
||||
IDivisionTeamRegistrationRepository DivisionTeamRegistrationRepository { get; }
|
||||
Task<int> SaveChangesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Runs operation inside a single database transaction, rolling back entirely with no partial writes if the operation throws.
|
||||
/// </summary>
|
||||
Task ExecuteInTransactionAsync(Func<Task> operation);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
namespace Application.Interfaces.Repositories;
|
||||
|
||||
public interface IVenueRepository : IGenericRepository<Venue>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.AuditLogs.Request;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
using Domain.Enums;
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Records and reads the sensitive-action audit trail.
|
||||
/// </summary>
|
||||
public interface IAuditService
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes an audit entry for action without ever throwing at the call site.
|
||||
/// </summary>
|
||||
Task LogAsync(
|
||||
AuditAction action,
|
||||
string? targetType = null,
|
||||
string? targetId = null,
|
||||
string? targetName = null,
|
||||
string? detail = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Returns audit entries, newest first by default, with pagination.
|
||||
/// </summary>
|
||||
Task<PaginatedResponse<AuditLog>> GetAuditLogsAsync(AuditLogFilteredRequest filter);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Application.DTOs.Auth.Response;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Token generator service for JWT and refresh tokens.
|
||||
/// </summary>
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<TokenResponse> GenerateJwtTokenAsync(IEnumerable<Claim> claims, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Application.DTOs.Auth.Request;
|
||||
using Application.DTOs.Auth.Response;
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Application boundary for authentication and user-registration flows.
|
||||
/// </summary>
|
||||
public interface IAuthenticationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Password login for the operator accounts, ADMIN and OWNER.
|
||||
/// </summary>
|
||||
Task<TokenResponse> LoginAsync(LogInUserRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a magic-link token, deferred to Phase 2 D6.
|
||||
/// </summary>
|
||||
Task<MagicLinkResponse> RequestMagicLinkAsync(MagicLinkRequest request, CancellationToken ct = default);
|
||||
Task<TokenResponse> MagicLinkLoginAsync(MagicLinkLoginRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Issues a guest JWT without any database interaction.
|
||||
/// </summary>
|
||||
Task<TokenResponse> GuestAsync(CancellationToken ct = default);
|
||||
Task<TokenResponse> RefreshAsync(RefreshTokenRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the password-reset token from the email link, sets the new password, clears MustChangePassword, and returns a ready-to-use JWT.
|
||||
/// </summary>
|
||||
Task<TokenResponse> ConfirmPasswordResetAsync(
|
||||
PasswordResetConfirmRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new user, where callerRole determines permitted target roles.
|
||||
/// </summary>
|
||||
Task<RegisterUserResponse> RegisterAsync(
|
||||
RegisterUserRequest request,
|
||||
string callerRole,
|
||||
Guid callerId,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a user by email only, with no password, and emails a magic activation link so the user sets their own password.
|
||||
/// </summary>
|
||||
Task<InviteUserResponse> InviteUserAsync(
|
||||
InviteUserRequest request,
|
||||
string callerRole,
|
||||
Guid callerId,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Consumes the activation token from the invitation email, sets the user's first password, enables login, and returns a ready-to-use JWT.
|
||||
/// </summary>
|
||||
Task<TokenResponse> ActivateAccountAsync(
|
||||
ActivateAccountRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Emails a password-reset magic link for the given email, completing silently with no user enumeration when no account matches.
|
||||
/// </summary>
|
||||
Task RequestPasswordResetAsync(
|
||||
RequestPasswordResetRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the caller's stored RefreshToken and RefreshTokenExpiryTime.
|
||||
/// </summary>
|
||||
Task LogoutAsync(Guid userId, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.BlogPosts.Request;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IBlogPostService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a blog post, deriving a unique slug from its title.
|
||||
/// </summary>
|
||||
/// <param name="blogPostEntity">The blog post entity to create.</param>
|
||||
/// <returns>The created blog post.</returns>
|
||||
Task<BlogPost> CreateBlogPostAsync(BlogPost blogPostEntity);
|
||||
|
||||
Task<BlogPost?> GetBlogPostByIdAsync(Guid blogPostId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a blog post by its id or its public slug, treating the value as an id when it parses as a GUID and otherwise looking it up as a slug.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The blog post's GUID id or its slug.</param>
|
||||
/// <param name="includeUnpublished">
|
||||
/// When false, the default for public callers, a draft post is treated as not found and null is returned; when true, for Admin or Owner, drafts are returned the same as any other post.
|
||||
/// </param>
|
||||
/// <returns>The blog post with the specified id or slug, or null if not found.</returns>
|
||||
Task<BlogPost?> GetBlogPostByIdOrSlugAsync(string idOrSlug, bool includeUnpublished = false);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a blog post as a no-op with no exception when id does not match any post.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the blog post to delete.</param>
|
||||
Task DeleteBlogPostAsync(Guid id);
|
||||
|
||||
Task UpdateBlogPostAsync(BlogPost blogPostEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves blog posts with pagination and filtering.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filtering and pagination request.</param>
|
||||
/// <param name="includeUnpublished">
|
||||
/// When false, the default for public callers, only published posts are returned; when true, for Admin or Owner, drafts are included too.
|
||||
/// </param>
|
||||
/// <returns>A paginated response containing the blog posts.</returns>
|
||||
Task<PaginatedResponse<BlogPost>> GetAllBlogPostsAsync(GetBlogPostsFilteredRequest filter, bool includeUnpublished = false);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Application.DTOs.Champions.Response;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the champion and podium of each competition, a zone division or the cross-division cup.
|
||||
/// </summary>
|
||||
public interface IChampionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes a division's podium from the playoff final and third-place match when the division has a playoff, or from the group-phase standings otherwise.
|
||||
/// </summary>
|
||||
/// <param name="divisionId">The id of the division.</param>
|
||||
/// <returns>The division's podium, or null when the division does not exist.</returns>
|
||||
Task<PodiumResponse?> GetDivisionPodiumAsync(Guid divisionId);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the podium of every division of a tournament so the caller sees the whole tournament at a glance.
|
||||
/// </summary>
|
||||
/// <param name="tournamentId">The id of the tournament.</param>
|
||||
/// <returns>One podium per division; empty when the tournament has no divisions.</returns>
|
||||
Task<List<PodiumResponse>> GetTournamentChampionsAsync(Guid tournamentId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the champion, 1st place, of every division of every FINISHED tournament, optionally scoped to a single season.
|
||||
/// </summary>
|
||||
/// <param name="seasonId">Optional season filter; when null, spans all seasons.</param>
|
||||
/// <returns>One row per crowned division champion.</returns>
|
||||
Task<List<ChampionHistoryResponse>> GetChampionsHistoryAsync(Guid? seasonId);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Application.DTOs.Club.Response;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the stable, cross-season club identity that per-season Team rows hang off of.
|
||||
/// </summary>
|
||||
public interface IClubService
|
||||
{
|
||||
/// <summary>
|
||||
/// Idempotently ensures every team is linked to a stable Club, creating one club per distinct team Name and linking each currently-unlinked team to it.
|
||||
/// </summary>
|
||||
/// <returns>How many clubs were created and teams linked, zeros on a re-run.</returns>
|
||||
Task<ClubBackfillResult> BackfillClubsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Idempotently links a single team to its stable club, creating the club if this is the first team with that name.
|
||||
/// </summary>
|
||||
/// <param name="team">A persisted team. Left untouched if already linked.</param>
|
||||
Task EnsureTeamLinkedToClubAsync(Team team);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a club and its trajectory across seasons: every per-season team that belongs to it and the tournaments each was registered in.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The club's GUID id or its slug.</param>
|
||||
/// <returns>The club history, or null when no club matches.</returns>
|
||||
Task<ClubHistoryResponse?> GetClubHistoryAsync(string idOrSlug);
|
||||
|
||||
/// <summary>
|
||||
/// Every club's stable identity summary, ordered by name — used to populate the "link to parent club" picker.
|
||||
/// </summary>
|
||||
Task<IEnumerable<ClubSummaryResponse>> GetAllClubsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Links a club as a squad of a parent institution club. Flat and one level deep: rejects linking a club to itself, linking to a parent that is itself already a squad, and linking a club that already has its own squads.
|
||||
/// </summary>
|
||||
/// <param name="childClubId">The squad club to link.</param>
|
||||
/// <param name="parentClubId">The institution club it becomes a squad of.</param>
|
||||
Task<ClubHistoryResponse> LinkClubToParentAsync(Guid childClubId, Guid parentClubId);
|
||||
|
||||
/// <summary>
|
||||
/// Clears a club's parent link, if any. Idempotent.
|
||||
/// </summary>
|
||||
/// <param name="childClubId">The club to unlink.</param>
|
||||
Task<ClubHistoryResponse> UnlinkClubParentAsync(Guid childClubId);
|
||||
|
||||
/// <summary>
|
||||
/// Renames a club. The club's slug never changes, so its public URL stays stable.
|
||||
/// </summary>
|
||||
/// <param name="clubId">The club to rename.</param>
|
||||
/// <param name="name">The new display name.</param>
|
||||
Task<ClubHistoryResponse> RenameClubAsync(Guid clubId, string name);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a club, blocking the delete while it still has teams or squad clubs linked to it.
|
||||
/// </summary>
|
||||
/// <param name="clubId">The club to delete.</param>
|
||||
Task DeleteClubAsync(Guid clubId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts access to the current caller's identity so application services can record who in the audit trail without depending on the web layer.
|
||||
/// </summary>
|
||||
public interface ICurrentUserAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// The authenticated caller's identifier, their email, or AuditConstants.SystemUser when no user is bound to the current execution context.
|
||||
/// </summary>
|
||||
string Actor { get; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Application.DTOs.DataMaintenance.Response;
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Admin-only tools for resetting the tournament-domain data to a clean, realistic sample state.
|
||||
/// </summary>
|
||||
public interface IDataMaintenanceService
|
||||
{
|
||||
/// <summary>
|
||||
/// Deletes every tournament-domain row inside one transaction, leaving Identity untouched.
|
||||
/// </summary>
|
||||
Task<DataWipeResult> WipeSampleDataAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Seeds 2 complete, distinct sample tournaments.
|
||||
/// </summary>
|
||||
Task<DataSeedResult> SeedSampleDataAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IDivisionRosterService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns every team currently enrolled in the division, independent of any stage placement.
|
||||
/// </summary>
|
||||
Task<List<Team>> GetRosterAsync(Guid divisionId);
|
||||
|
||||
/// <summary>
|
||||
/// Enrolls teams in a division, skipping already-registered teams and rejecting a conflicting cross-division registration.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown as a 409 when a team already holds a conflicting registration, or the tournament has already started.
|
||||
/// </exception>
|
||||
Task<List<DivisionTeamRegistration>> EnrollTeamsAsync(Guid divisionId, List<Guid> teamIds);
|
||||
|
||||
/// <summary>
|
||||
/// Removes teams from a division's roster, cascading to delete their stage placements in that division first.
|
||||
/// </summary>
|
||||
Task UnenrollTeamsAsync(Guid divisionId, List<Guid> teamIds);
|
||||
|
||||
/// <summary>
|
||||
/// Replaces a division's sub-group stages with a new count, re-balancing the untouched roster across them.
|
||||
/// </summary>
|
||||
Task<List<Stage>> RebuildSubGroupsAsync(Guid divisionId, int subGroupCount);
|
||||
|
||||
/// <summary>
|
||||
/// Clears every current sub-group placement and re-deals the whole roster in a fresh balanced distribution.
|
||||
/// </summary>
|
||||
Task AutoDistributeRosterAsync(Guid divisionId);
|
||||
|
||||
/// <summary>
|
||||
/// Manually moves one enrolled team from one sub-group to another, re-validating only the minimum sub-group size.
|
||||
/// </summary>
|
||||
Task ReassignTeamToSubGroupAsync(Guid teamId, Guid fromStageId, Guid toStageId);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.Divisions.Request;
|
||||
using Application.Utils.Helper.Standings;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IDivisionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a division and generates its unique slug from the name.
|
||||
/// </summary>
|
||||
/// <param name="divisionEntity">The division entity to create.</param>
|
||||
/// <returns>The created division.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the tournament is not TournamentStatus.OpenForRegistration, since structure is frozen once registration closes, when the division's Division.Category does not match its tournament's category, or when its playoff mappings are invalid.
|
||||
/// </exception>
|
||||
Task<Division> CreateDivisionAsync(Division divisionEntity);
|
||||
|
||||
Task<Division?> GetFullDivisionByIdAsync(Guid divisionId);
|
||||
|
||||
Task<Division?> GetSimpleDivisionByIdAsync(Guid divisionId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a division by its id or its slug asynchronously, treating the value as an id when it parses as a GUID and otherwise looking it up as a slug.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The division's GUID id or its slug.</param>
|
||||
/// <returns>The matching division, or null if not found.</returns>
|
||||
Task<Division?> GetSimpleDivisionByIdOrSlugAsync(string idOrSlug);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a division, blocked whenever its match, statistics, or point-deduction history exists or would be destroyed silently.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the division to delete.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the division has any finished match or point deduction, or its tournament has already started, Ongoing or Finished, or was Canceled.
|
||||
/// </exception>
|
||||
Task DeleteDivisionAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a division, re-validating it against the same tournament-status and category rules enforced on create. See CreateDivisionAsync.
|
||||
/// </summary>
|
||||
/// <param name="divisionEntity">The division entity with updated values.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the tournament no longer allows structural edits, the
|
||||
/// category no longer matches, or the playoff mappings are invalid.
|
||||
/// </exception>
|
||||
Task UpdateDivisionAsync(Division divisionEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves divisions with pagination and filtering asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filtering and pagination request.</param>
|
||||
/// <returns>A paginated response containing the divisions.</returns>
|
||||
Task<PaginatedResponse<Division>> GetAllDivisionsAsync(GetDivisionsFilteredRequest filter);
|
||||
|
||||
/// <summary>
|
||||
/// Computes standings for a division from its Group stage's finished matches; elimination-stage matches do not feed a standings table.
|
||||
/// </summary>
|
||||
/// <param name="divisionId">The id of the division.</param>
|
||||
/// <returns>One Position per team with at least one finished Group-stage match; empty if the division has no Group stage or no finished matches yet.</returns>
|
||||
Task<List<Position>> GetPositionsByDivisionIdAsync(Guid divisionId);
|
||||
|
||||
/// <summary>
|
||||
/// Computes standings for a division split by Group stage, with a regular zone returning a single entry and a multi-group cross-division cup returning one entry per internal group.
|
||||
/// </summary>
|
||||
/// <param name="divisionId">The id of the division.</param>
|
||||
/// <returns>One GroupStandings per Group stage.</returns>
|
||||
Task<List<GroupStandings>> GetGroupStandingsByDivisionIdAsync(Guid divisionId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns every team registered to the tournament that does not yet belong to any division, regular or cross-division cup.
|
||||
/// </summary>
|
||||
/// <param name="tournamentId">The id of the tournament.</param>
|
||||
Task<List<Team>> GetUnassignedTeamsAsync(Guid tournamentId);
|
||||
|
||||
/// <summary>
|
||||
/// Reassigns a division to a different tournament, moving everything under it along with it.
|
||||
/// </summary>
|
||||
/// <param name="division">The division to reassign. Its Tournament navigation and TournamentId are mutated in place.</param>
|
||||
/// <param name="tournamentId">The id of the tournament the division should belong to.</param>
|
||||
/// <returns>True if the target tournament exists and the division was reassigned in memory; false if no tournament with that id exists.</returns>
|
||||
Task<bool> TryAssignTournamentAsync(Division division, Guid tournamentId);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IEmailService
|
||||
{
|
||||
Task SendPasswordResetAsync(
|
||||
string toEmail, string toUsername, string resetLink,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task SendMagicLinkAsync(
|
||||
string toEmail, string toUsername, string magicLink,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task SendWelcomeSetPasswordAsync(
|
||||
string toEmail, string toUsername, string setPasswordLink,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.MatchSeries.Request;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing best-of-N playoff series between two teams at a single bracket round.
|
||||
/// </summary>
|
||||
public interface IMatchSeriesService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a series by its identifier, including its games.
|
||||
/// </summary>
|
||||
/// <param name="seriesId">Unique identifier of the series.</param>
|
||||
/// <returns>The found series or null if it does not exist.</returns>
|
||||
Task<MatchSeries?> GetSeriesByIdAsync(Guid seriesId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a paginated and filtered list of series.
|
||||
/// </summary>
|
||||
/// <param name="filter">Object containing parameters to filter, sort, and paginate the results.</param>
|
||||
/// <returns>A paginated response with the list of series.</returns>
|
||||
Task<PaginatedResponse<MatchSeries>> GetAllSeriesAsync(GetMatchSeriesFilteredRequest filter);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new best-of-N series between two teams at a stage, copying the stage's BestOf value onto the series.
|
||||
/// </summary>
|
||||
/// <param name="stageId">The stage, round, the series belongs to.</param>
|
||||
/// <param name="homeTeamId">The home team.</param>
|
||||
/// <param name="visitorTeamId">The visitor team.</param>
|
||||
/// <returns>The created series entity.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the stage does not exist, the two teams are the same,
|
||||
/// either team is not assigned to the stage's division, or a series
|
||||
/// between these two teams already exists for this stage.
|
||||
/// </exception>
|
||||
Task<MatchSeries> CreateSeriesAsync(Guid stageId, Guid homeTeamId, Guid visitorTeamId);
|
||||
|
||||
/// <summary>
|
||||
/// Schedules the next game of an existing series.
|
||||
/// </summary>
|
||||
/// <param name="seriesId">The series to add a game to.</param>
|
||||
/// <param name="matchDate">The date of the game.</param>
|
||||
/// <param name="venueId">Optional venue for the game.</param>
|
||||
/// <returns>The created game, a Match entity.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the series does not exist, is already decided, or
|
||||
/// already has BestOf games scheduled.
|
||||
/// </exception>
|
||||
Task<Match> AddGameToSeriesAsync(Guid seriesId, DateTime matchDate, Guid? venueId);
|
||||
|
||||
/// <summary>
|
||||
/// Recomputes and persists the series' winner based on its finished games.
|
||||
/// </summary>
|
||||
/// <param name="seriesId">The series to recompute.</param>
|
||||
Task RecalculateSeriesWinnerAsync(Guid seriesId);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.Match.Request;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IMatchService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a match and generates its unique slug from the home and visitor team names and match date.
|
||||
/// </summary>
|
||||
Task<Match> CreateMatchAsync(Match matchEntity);
|
||||
|
||||
Task<Match?> GetMatchByIdAsync(Guid matchId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a match by its id or its slug, treating the value as an id when it parses as a GUID and otherwise looking it up as a slug.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The match's GUID id or its slug.</param>
|
||||
/// <returns>The matching match, or null if not found.</returns>
|
||||
Task<Match?> GetMatchByIdOrSlugAsync(string idOrSlug);
|
||||
|
||||
Task<Match?> GetMatchByIdWithScorersAsync(Guid matchId);
|
||||
|
||||
Task UpdateMatchAsync(Match matchEntity);
|
||||
|
||||
/// <summary>
|
||||
/// True when another match, not excludeMatchId, is scheduled at the same venue less than 2 hours from matchDate, since two matches on one court must be at least 2 hours apart.
|
||||
/// </summary>
|
||||
Task<bool> HasVenueScheduleConflictAsync(Guid venueId, DateTime matchDate, Guid excludeMatchId);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a decisive final result for a match, rejecting a tied score.
|
||||
/// </summary>
|
||||
Task<Match?> LoadMatchResultAsync(Guid matchId, int homeScore, int visitorScore);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a walkover result to a match, awarding the regulation default to the present team.
|
||||
/// </summary>
|
||||
Task<Match?> LoadWalkOverAsync(Guid matchId, Guid presentTeamId, int? presentTeamScore);
|
||||
|
||||
/// <summary>
|
||||
/// Reprograms or suspends a match, marking it MatchStatus.Suspended and optionally moving it to a new calendar date.
|
||||
/// </summary>
|
||||
Task<Match?> SuspendMatchAsync(Guid matchId, DateTime? newMatchDate);
|
||||
|
||||
Task DeleteMatchAsync(Guid id);
|
||||
|
||||
Task<PaginatedResponse<Match>> GetAllMatchesAsync(GetMatchesFilteredRequest filter);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves every match of a stage ordered by matchday, round 1 first then round 2 and so on.
|
||||
/// </summary>
|
||||
Task<List<Match>> GetStageMatchesByRoundAsync(Guid stageId);
|
||||
|
||||
Task<List<Match>> CreateAutomatedMatchesAsync(Guid stageId);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Application.DTOs.MedicalRecord.Response;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the medical-record and eligibility state of a player's season registration.
|
||||
/// </summary>
|
||||
public interface IMedicalRecordService
|
||||
{
|
||||
/// <summary>
|
||||
/// Records a just-uploaded medical-record file reference on the player's season registration.
|
||||
/// </summary>
|
||||
Task<MedicalRecordResponse> RecordUploadAsync(
|
||||
Guid playerId, Guid teamId, Guid tournamentId, string fileReference, string fileName, string actor);
|
||||
|
||||
/// <summary>
|
||||
/// Approves or rejects the medical record.
|
||||
/// </summary>
|
||||
Task<MedicalRecordResponse> ReviewAsync(
|
||||
Guid playerId, Guid teamId, Guid tournamentId, bool approve, string? reason, string actor);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current medical-record and eligibility state for a player's season registration, or null when no such registration exists.
|
||||
/// </summary>
|
||||
Task<MedicalRecordResponse?> GetAsync(Guid playerId, Guid teamId, Guid tournamentId);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.PlayerSanction.Request;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IPlayerSanctionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a sanction and assigns it a unique slug derived from its subject's resolved name and issue date, so a player, team, or staff sanction all get a readable slug regardless of subject type.
|
||||
/// </summary>
|
||||
/// <param name="playerSanctionEntity">The player sanction entity to create.</param>
|
||||
/// <returns>The created player sanction.</returns>
|
||||
Task<PlayerSanction> CreatePlayerSanctionAsync(PlayerSanction playerSanctionEntity);
|
||||
|
||||
Task<PlayerSanction?> GetPlayerSanctionByIdAsync(Guid playerSanctionId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a player sanction by its id or its slug, treating the value as an id when it parses as a GUID and otherwise looking it up as a slug.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The sanction's GUID id or its slug.</param>
|
||||
/// <returns>The matching player sanction, or null if not found.</returns>
|
||||
Task<PlayerSanction?> GetPlayerSanctionByIdOrSlugAsync(string idOrSlug);
|
||||
|
||||
Task DeletePlayerSanctionAsync(Guid id);
|
||||
|
||||
Task UpdatePlayerSanctionAsync(PlayerSanction playerSanctionEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves expired player sanctions as of a specific date asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="date">The date to check for expired sanctions.</param>
|
||||
/// <returns>A collection of expired player sanctions.</returns>
|
||||
Task<IEnumerable<PlayerSanction>> GetExpiredSanctionsAsync(DateTime date);
|
||||
|
||||
/// <summary>
|
||||
/// Computes how many FECHAS, jornadas, of a sanction are still to be served, based on the subject team's finished rounds since the sanction was issued.
|
||||
/// </summary>
|
||||
/// <param name="sanction">The sanction to evaluate.</param>
|
||||
/// <returns>The fechas remaining, or null when not computable by rounds.</returns>
|
||||
Task<int?> GetFechasRemainingAsync(PlayerSanction sanction);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a player has any active sanction, one with fechas still to be served, so eligibility stays consistent with the fechas-based rule.
|
||||
/// </summary>
|
||||
/// <param name="playerId">The player to check.</param>
|
||||
/// <returns>True when the player has at least one active sanction.</returns>
|
||||
Task<bool> HasActiveSanctionAsync(Guid playerId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves player sanctions with pagination and filtering asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filtering and pagination request.</param>
|
||||
/// <returns>A paginated response containing the player sanctions.</returns>
|
||||
Task<PaginatedResponse<PlayerSanction>> GetPlayerSanctionsAsync(GetPlayerSanctionsFilteredRequest filter);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the human-readable subject of a sanction into the three mutually-exclusive display fields the response exposes.
|
||||
/// </summary>
|
||||
/// <param name="sanction">The sanction whose subject to resolve.</param>
|
||||
/// <returns>
|
||||
/// The player, team and staff display names; exactly one is non-null for a
|
||||
/// well-formed sanction, the others are null.
|
||||
/// </returns>
|
||||
Task<(string? PlayerFullName, string? TeamName, string? StaffName)> ResolveSubjectAsync(PlayerSanction sanction);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.Player.Request;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IPlayerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new Player and registers them to tournamentId on the player's team.
|
||||
/// </summary>
|
||||
/// <param name="playerEntity">The Player entity to create.</param>
|
||||
/// <param name="tournamentId">
|
||||
/// The season, Tournament, to register the player's team assignment to, normally the player's team's current TournamentId.
|
||||
/// </param>
|
||||
/// <returns>The created Player.</returns>
|
||||
Task<Player> CreatePlayerAsync(Player playerEntity, Guid tournamentId);
|
||||
|
||||
Task<Player?> GetPlayerByIdAsync(Guid playerId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a Player by its id or its slug, treating the value as an id when it parses as a GUID and otherwise looking it up as a slug.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The player's GUID id or its slug.</param>
|
||||
/// <returns>The matching player, or null if not found.</returns>
|
||||
Task<Player?> GetPlayerByIdOrSlugAsync(string idOrSlug);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a Player and keeps their season-scoped roster registration in sync.
|
||||
/// </summary>
|
||||
/// <param name="playerEntity">The Player entity, with updated fields already applied.</param>
|
||||
/// <param name="tournamentId">The season, Tournament, the player's current team belongs to.</param>
|
||||
Task UpdatePlayerAsync(Player playerEntity, Guid tournamentId);
|
||||
|
||||
Task DeletePlayerAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a player onto a team's roster for a specific tournament, enforcing the roster invariants.
|
||||
/// </summary>
|
||||
/// <param name="playerId">The player to register.</param>
|
||||
/// <param name="teamId">The team to register the player onto.</param>
|
||||
/// <param name="tournamentId">The tournament, season, the registration belongs to.</param>
|
||||
/// <param name="jerseyNumber">The player's dorsal for this team/season, or null.</param>
|
||||
/// <returns>The created or updated registration.</returns>
|
||||
/// <exception cref="System.InvalidOperationException">
|
||||
/// Thrown when any of the roster invariants is violated.
|
||||
/// </exception>
|
||||
Task<PlayerTeamRegistration> RegisterPlayerToTeamAsync(
|
||||
Guid playerId, Guid teamId, Guid tournamentId, int? jerseyNumber = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves players with pagination and filtering.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filtering and pagination request.</param>
|
||||
/// <returns>A paginated response containing the players.</returns>
|
||||
Task<PaginatedResponse<Player>> GetAllPlayersAsync(PlayerFilterRequestBase filter);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.PlayerStatistic.Request;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IPlayerStatisticService
|
||||
{
|
||||
Task<PlayerStatistic> CreatePlayerStatisticAsync(PlayerStatistic playerStatisticEntity);
|
||||
|
||||
Task<PlayerStatistic?> GetPlayerStatisticByIdAsync(Guid playerStatisticId);
|
||||
|
||||
Task DeletePlayerStatisticAsync(Guid id);
|
||||
|
||||
Task UpdatePlayerStatisticAsync(PlayerStatistic playerStatisticEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a paginated, filtered list of player statistics.
|
||||
/// </summary>
|
||||
Task<PaginatedResponse<PlayerStatistic>> GetPlayerStatisticsAsync(GetPlayerStatisticsFilteredRequest filter);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a whole team's coherent scoring sheet for a match.
|
||||
/// </summary>
|
||||
Task<List<PlayerStatistic>> LoadTeamMatchSheetAsync(LoadMatchSheetRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Finishes a match by loading both teams' scoring sheets in one operation.
|
||||
/// </summary>
|
||||
/// <returns>The finalized match, or null if no match with that id exists.</returns>
|
||||
Task<Match?> LoadMatchResultFromSheetsAsync(LoadMatchResultFromSheetsRequest request);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Application.DTOs.Roster.Response;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Clones a roster from a previous season's team into a new season's team so admins don't re-enter every player each year.
|
||||
/// </summary>
|
||||
public interface IRosterCopyService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new season registration on targetTeamId for targetTournamentId for every player registered to the source team in the source season.
|
||||
/// </summary>
|
||||
/// <param name="sourceTeamId">The past-season team to copy from.</param>
|
||||
/// <param name="sourceTournamentId">The season the source roster belongs to.</param>
|
||||
/// <param name="targetTeamId">The new-season team to copy into.</param>
|
||||
/// <param name="targetTournamentId">The new season the roster is cloned into.</param>
|
||||
/// <returns>How many registrations were created and how many were skipped.</returns>
|
||||
Task<RosterCopyResult> CopyRosterAsync(
|
||||
Guid sourceTeamId, Guid sourceTournamentId, Guid targetTeamId, Guid targetTournamentId);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.Match.Request;
|
||||
using Application.DTOs.Scorer.Request;
|
||||
using Application.DTOs.Scorer.Response;
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IScorerService
|
||||
{
|
||||
Task<PaginatedResponse<ScorerByPlayerResponse>> GetAllScorersByPlayerAsync(GetScorerFilteredRequest filter);
|
||||
|
||||
Task<PaginatedResponse<ScorerByTeamResponse>> GetAllScorersByTeamAsync(GetMatchesFilteredRequest filter);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service for managing Seasons, Temporadas.
|
||||
/// </summary>
|
||||
public interface ISeasonService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new Season asynchronously, generating its unique slug from the name.
|
||||
/// </summary>
|
||||
/// <param name="seasonEntity">The Season entity to create.</param>
|
||||
/// <returns>The created Season.</returns>
|
||||
Task<Season> CreateSeasonAsync(Season seasonEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a Season by its id asynchronously, including its tournaments.
|
||||
/// </summary>
|
||||
/// <param name="seasonId">The id of the Season to retrieve.</param>
|
||||
/// <returns>The Season with the specified id, or null if not found.</returns>
|
||||
Task<Season?> GetSeasonByIdAsync(Guid seasonId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a Season by its id or its slug asynchronously, treating the value as an id when it parses as a GUID and otherwise looking it up as a slug.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The season's GUID id or its slug.</param>
|
||||
/// <returns>The matching season, or null if not found.</returns>
|
||||
Task<Season?> GetSeasonByIdOrSlugAsync(string idOrSlug);
|
||||
|
||||
Task UpdateSeasonAsync(Season seasonEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a season and every tournament it groups, routing each tournament through ITournamentService.DeleteTournamentAsync rather than a bare row delete.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the Season to delete.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Propagated from ITournamentService.DeleteTournamentAsync
|
||||
/// when any grouped tournament has started or has played history and
|
||||
/// cannot be deleted.
|
||||
/// </exception>
|
||||
Task DeleteSeasonAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all seasons asynchronously, including their tournaments.
|
||||
/// </summary>
|
||||
/// <returns>All seasons.</returns>
|
||||
Task<IEnumerable<Season>> GetAllSeasonsAsync();
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.Stage.Request;
|
||||
using Application.DTOs.Stage.Response;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IStageService
|
||||
{
|
||||
Task<Stage?> GetStageByIdAsync(Guid stageId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a stage by its id or its slug asynchronously, treating the value as an id when it parses as a GUID and otherwise looking it up as a slug.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The stage's GUID id or its slug.</param>
|
||||
/// <returns>The matching stage, or null if not found.</returns>
|
||||
Task<Stage?> GetStageByIdOrSlugAsync(string idOrSlug);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a paginated and filtered list of Stages.
|
||||
/// </summary>
|
||||
/// <param name="filter">Object containing parameters to filter, sort, and paginate the results.</param>
|
||||
/// <returns>A paginated response with the list of Stages.</returns>
|
||||
Task<PaginatedResponse<Stage>> GetAllStagesAsync(GetStagesFilteredRequest filter);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a stage, blocked once its tournament has started.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the stage to delete.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the stage's tournament has already started or was canceled.
|
||||
/// </exception>
|
||||
Task DeleteStageAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a stage, blocked once its tournament has started.
|
||||
/// </summary>
|
||||
/// <param name="stageEntity">Stage entity with updated data.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown, mapped to 409, when the tournament has already started or was canceled.
|
||||
/// </exception>
|
||||
Task UpdateStageAsync(Stage stageEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a stage, blocked once its tournament has started.
|
||||
/// </summary>
|
||||
/// <param name="stageEntity">Stage entity to create.</param>
|
||||
/// <returns>The created Stage entity.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when a stage with the same name already exists in the division.
|
||||
/// </exception>
|
||||
Task<Stage> CreateStageAsync(Stage stageEntity);
|
||||
|
||||
Task AssignTeamsToStageAsync(Stage stage, List<Guid>? teamIds = null, bool auto = false);
|
||||
Task UnassignTeamsFromStageAsync(Stage stage, List<Guid> teamIds);
|
||||
|
||||
/// <summary>
|
||||
/// Seeds the first-round matches of an elimination stage using the division's group-stage standings and the classic bracket seed order, so the top seeds only meet in the final.
|
||||
/// </summary>
|
||||
/// <param name="stageId">The elimination stage to seed.</param>
|
||||
/// <returns>The stage's matches, now seeded with home/visitor teams.</returns>
|
||||
Task<List<Match>> SeedKnockoutStageAsync(Guid stageId);
|
||||
|
||||
/// <summary>
|
||||
/// Seeds every playoff cup of a division from its final group-stage standings using the division's position-range mapping.
|
||||
/// </summary>
|
||||
/// <param name="divisionId">The division whose group stage has finished.</param>
|
||||
/// <returns>The seeded matches per destination cup, keyed by BracketName.</returns>
|
||||
Task<Dictionary<string, List<Match>>> SeedPlayoffCupsAsync(Guid divisionId);
|
||||
|
||||
/// <summary>
|
||||
/// Automatically seeds a division's playoff cups once every group-stage match is finished.
|
||||
/// </summary>
|
||||
/// <param name="finishedMatchStageId">The stage of the match that just finished.</param>
|
||||
Task TryAutoSeedPlayoffPhaseAsync(Guid finishedMatchStageId);
|
||||
|
||||
/// <summary>
|
||||
/// Pushes each newly-decided bracket slot's winner into its immediate next round within the same cup.
|
||||
/// </summary>
|
||||
/// <param name="decidedStageId">The stage whose slots just got decided.</param>
|
||||
Task TryAdvanceStageWinnerAsync(Guid decidedStageId);
|
||||
|
||||
/// <summary>
|
||||
/// Computes a first-round pairing for a groupless bracket without persisting it, returning a signed token that replays the exact same order on commit.
|
||||
/// </summary>
|
||||
/// <param name="stageId">The bracket's first-round stage.</param>
|
||||
/// <param name="mode">Whether the order is shuffled server-side or supplied manually.</param>
|
||||
/// <param name="manualOrder">The explicit team order, required and validated as a roster permutation when mode is Manual.</param>
|
||||
/// <returns>The previewed pairing together with a draw token that commit can replay.</returns>
|
||||
Task<DrawPreviewResult> PreviewDrawAsync(Guid stageId, DrawMode mode, List<Guid>? manualOrder = null);
|
||||
|
||||
/// <summary>
|
||||
/// Seeds a groupless bracket from a previewed token or a manual order, stamping DrawnAt and auditing the draw.
|
||||
/// </summary>
|
||||
/// <param name="stageId">The bracket's first-round stage.</param>
|
||||
/// <param name="mode">Whether the order comes from a previewed random draw or a manual order.</param>
|
||||
/// <param name="drawToken">The token returned by a prior preview, required and verified when mode is Random.</param>
|
||||
/// <param name="manualOrder">The explicit team order, required and validated as a roster permutation when mode is Manual.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown as a 409 when a real match of this bracket has already been played, or the draw token is missing, tampered, or mismatched.
|
||||
/// </exception>
|
||||
/// <returns>The bracket's first-round matches, now seeded with home/visitor teams.</returns>
|
||||
Task<List<Match>> CommitDrawAsync(Guid stageId, DrawMode mode, string? drawToken = null, List<Guid>? manualOrder = null);
|
||||
|
||||
/// <summary>
|
||||
/// Replaces a regular division's sub-group stage layer with a new count, re-balancing the untouched roster across it.
|
||||
/// </summary>
|
||||
/// <param name="divisionId">The division whose sub-groups are rebuilt.</param>
|
||||
/// <param name="subGroupCount">The new number of sub-groups, at least 1.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown as a 409 when the tournament has already started, the roster is too small for the
|
||||
/// requested count, or the division already carries a position-range playoff cup.
|
||||
/// </exception>
|
||||
/// <returns>The newly created sub-group stages.</returns>
|
||||
Task<List<Stage>> RebuildSubGroupsAsync(Guid divisionId, int subGroupCount);
|
||||
|
||||
/// <summary>
|
||||
/// Clears every current sub-group placement and re-runs balanced distribution over the division's whole roster.
|
||||
/// </summary>
|
||||
/// <param name="divisionId">The division whose sub-groups are re-balanced.</param>
|
||||
Task AutoDistributeRosterAsync(Guid divisionId);
|
||||
|
||||
/// <summary>
|
||||
/// Manually moves one enrolled team from one sub-group to another of the same division, re-validating only the minimum sub-group size.
|
||||
/// </summary>
|
||||
/// <param name="teamId">The team to move.</param>
|
||||
/// <param name="fromStageId">The sub-group the team currently belongs to.</param>
|
||||
/// <param name="toStageId">The sub-group the team moves into.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown as a 409 when the tournament has already started, the team is not placed in
|
||||
/// fromStageId, the two stages belong to different divisions, or the move would drop the
|
||||
/// source sub-group below the minimum size.
|
||||
/// </exception>
|
||||
Task ReassignTeamToSubGroupAsync(Guid teamId, Guid fromStageId, Guid toStageId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IStageTeamMatchService
|
||||
{
|
||||
Task<List<StageTeamMatch>> GetAllStageTeamMatchByStageId(Guid stageId);
|
||||
Task InsertRangeAsync(List<StageTeamMatch> stageTeamMatches);
|
||||
Task RemoveWhereAsync(Expression<Func<StageTeamMatch, bool>> predicate);
|
||||
|
||||
/// <summary>
|
||||
/// True only when every id in TeamIds has a matching assignment row for the stage.
|
||||
/// </summary>
|
||||
Task<bool> AllTeamsAssignedToStage(Guid stageId, List<Guid> TeamIds);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Application.DTOs.Statistics.Response;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Historical player statistics: per-season and overall aggregations for a single person across every season.
|
||||
/// </summary>
|
||||
public interface IStatisticsService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the player's statistic card, or null if the player is unknown.
|
||||
/// </summary>
|
||||
Task<PlayerStatisticCardResponse?> GetPlayerCardAsync(Guid playerId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the player's cross-season history, or null if the player is unknown.
|
||||
/// </summary>
|
||||
Task<PlayerHistoryResponse?> GetPlayerHistoryAsync(Guid playerId);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages disciplinary point deductions, deducción de puntos, applied to teams within a division.
|
||||
/// </summary>
|
||||
public interface ITeamPointDeductionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new point deduction.
|
||||
/// </summary>
|
||||
/// <param name="deduction">The deduction to create.</param>
|
||||
/// <returns>The created deduction, with the team navigation loaded.</returns>
|
||||
Task<TeamPointDeduction> CreateAsync(TeamPointDeduction deduction);
|
||||
|
||||
/// <summary>
|
||||
/// Returns every point deduction for a division, newest first, with each deduction's team loaded so the caller can show the team's name.
|
||||
/// </summary>
|
||||
/// <param name="divisionId">The division whose deductions to list.</param>
|
||||
Task<List<TeamPointDeduction>> GetByDivisionIdAsync(Guid divisionId);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a point deduction by its id. No-op when it does not exist.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the deduction to remove.</param>
|
||||
Task DeleteAsync(Guid id);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.Team.Request;
|
||||
using Application.DTOs.Team.Response;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface ITeamService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a team, generates its unique slug from the name, and links it to its stable cross-season Club.
|
||||
/// </summary>
|
||||
/// <param name="teamEntity">The team entity to create.</param>
|
||||
/// <returns>The created team.</returns>
|
||||
Task<Team> CreateTeamAsync(Team teamEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a team by its id, with its Players roster scoped to one season.
|
||||
/// </summary>
|
||||
/// <param name="teamId">The id of the team to retrieve.</param>
|
||||
/// <param name="tournamentId">
|
||||
/// The season whose roster to attach. Defaults to the team's own current TournamentId when omitted, so callers get today's roster by default. A team with no current tournament, TournamentId null, and no explicit tournamentId gets an empty roster.
|
||||
/// </param>
|
||||
/// <returns>The team with the specified id, or null if not found.</returns>
|
||||
Task<Team?> GetTeamByIdAsync(Guid teamId, Guid? tournamentId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a team by its id or its slug, with its Players roster scoped to one season, treating the value as an id when it parses as a GUID and otherwise looking it up as a slug.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The team's GUID id or its slug.</param>
|
||||
/// <param name="tournamentId">
|
||||
/// The season whose roster to attach. Defaults to the team's own current
|
||||
/// TournamentId when omitted.
|
||||
/// </param>
|
||||
/// <returns>The matching team, or null if not found.</returns>
|
||||
Task<Team?> GetTeamByIdOrSlugAsync(string idOrSlug, Guid? tournamentId = null);
|
||||
|
||||
Task UpdateTeamAsync(Team teamEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Guards a team's identity edit, freezing Team.Name and Team.ThreeLetterCode while the team is participating in an Ongoing tournament.
|
||||
/// </summary>
|
||||
/// <param name="existingTeam">The team as currently persisted, original identity and current TournamentId, read BEFORE the request is mapped over it.</param>
|
||||
/// <param name="requestedName">The requested new name, or null when the request does not change the name.</param>
|
||||
/// <param name="requestedThreeLetterCode">The requested new three-letter code, or null when the request does not change it.</param>
|
||||
/// <exception cref="System.InvalidOperationException">Thrown, mapped to 409, when an identity change is attempted while the team's current tournament is Ongoing.</exception>
|
||||
Task EnsureTeamIdentityEditableAsync(Team existingTeam, string? requestedName, string? requestedThreeLetterCode);
|
||||
|
||||
Task UpdateTeamsAsync(IEnumerable<Team> teams);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a team, guarding its competitive history.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the team to delete.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown, mapped to 409, when the team has match history, sanctions, point deductions, tournament registrations, or roster players with their own history.
|
||||
/// </exception>
|
||||
Task DeleteTeamAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a paginated list of teams based on filtering criteria.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filtering criteria.</param>
|
||||
/// <returns>A paginated response containing the filtered teams.</returns>
|
||||
Task<PaginatedResponse<Team>> GetAllTeamsAsync(GetTeamsFilteredRequest filter);
|
||||
|
||||
Task RegisterTeamsToTournamentAsync(Tournament tournament, List<Guid> teamIds);
|
||||
|
||||
/// <summary>
|
||||
/// Enrolls a single team into tournament's registration phase, atomically.
|
||||
/// </summary>
|
||||
/// <param name="tournament">The tournament to enroll the team into. Must be OpenForRegistration.</param>
|
||||
/// <param name="existingTeamId">The existing team to enroll, or null.</param>
|
||||
/// <param name="newTeamName">The name of a brand-new team to create and enroll, or null.</param>
|
||||
/// <param name="copyRosterFromTournamentId">Optional source season whose roster to copy.</param>
|
||||
/// <returns>The enrolled team, with its roster scoped to this tournament.</returns>
|
||||
Task<Team> EnrollTeamAsync(
|
||||
Tournament tournament,
|
||||
Guid? existingTeamId,
|
||||
string? newTeamName,
|
||||
Guid? copyRosterFromTournamentId);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a team from a tournament, atomically.
|
||||
/// </summary>
|
||||
/// <param name="tournament">The tournament to remove the team from.</param>
|
||||
/// <param name="teamId">The id of the team to remove.</param>
|
||||
/// <exception cref="System.InvalidOperationException">The tournament is not in a phase that allows removing teams.</exception>
|
||||
/// <exception cref="System.Collections.Generic.KeyNotFoundException">The team is not enrolled in the tournament.</exception>
|
||||
Task UnenrollTeamAsync(Tournament tournament, Guid teamId);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the team's current group-stage standing row for a tournament, powering the public team-profile summary card.
|
||||
/// </summary>
|
||||
/// <param name="teamId">The team whose standing to locate.</param>
|
||||
/// <param name="tournamentId">
|
||||
/// The tournament to look in; when null the team has no season context and
|
||||
/// null is returned.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The team's standing row, or null when the team is in no group-stage
|
||||
/// standing for the tournament, whether playoff-only or with no finished matches.
|
||||
/// </returns>
|
||||
Task<TeamSummaryResponse?> GetTeamSummaryAsync(Guid teamId, Guid? tournamentId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the team's matches, as home or visitor, in a tournament, oriented from the team's perspective and ordered by match date ascending.
|
||||
/// </summary>
|
||||
/// <param name="teamId">The team whose matches to list.</param>
|
||||
/// <param name="tournamentId">
|
||||
/// The tournament to scope the matches to; when null an empty list is
|
||||
/// returned.
|
||||
/// </param>
|
||||
/// <returns>The team's matches, oldest first; empty when there are none.</returns>
|
||||
Task<List<TeamMatchResponse>> GetTeamMatchesAsync(Guid teamId, Guid? tournamentId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns every tournament the team has participated in, enriched with season info, newest first.
|
||||
/// </summary>
|
||||
/// <param name="teamId">The team whose participation history to list.</param>
|
||||
/// <param name="currentTournamentId">
|
||||
/// The team's current tournament pointer, used to flag the current
|
||||
/// participation; when null no entry is flagged current.
|
||||
/// </param>
|
||||
/// <returns>The team's tournament participations.</returns>
|
||||
Task<List<TeamParticipationResponse>> GetTeamParticipationsAsync(Guid teamId, Guid? currentTournamentId);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages a team's technical staff, cuerpo técnico, DT and Asistente, scoped per team and tournament.
|
||||
/// </summary>
|
||||
public interface ITeamStaffService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new staff member.
|
||||
/// </summary>
|
||||
/// <param name="staff">The staff member to create.</param>
|
||||
/// <returns>The created staff member, with the team navigation loaded.</returns>
|
||||
Task<TeamStaff> CreateAsync(TeamStaff staff);
|
||||
|
||||
/// <summary>
|
||||
/// Returns every staff member for a team within a tournament, ordered by DateCreated ascending.
|
||||
/// </summary>
|
||||
/// <param name="teamId">The team whose staff to list.</param>
|
||||
/// <param name="tournamentId">The tournament, season, to scope the staff to.</param>
|
||||
Task<List<TeamStaff>> GetByTeamAndTournamentAsync(Guid teamId, Guid tournamentId);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a staff member by their id. No-op when it does not exist.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the staff member to remove.</param>
|
||||
Task DeleteAsync(Guid id);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.Tournament.Request;
|
||||
using Application.DTOs.Tournament.Response;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface ITournamentService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a tournament and generates its unique slug from the name.
|
||||
/// </summary>
|
||||
/// <param name="tournamentEntity">The Tournament entity to create.</param>
|
||||
/// <returns>The created Tournament.</returns>
|
||||
Task<Tournament> CreateTournamentAsync(Tournament tournamentEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a whole tournament in a single transaction, reusing the granular create logic.
|
||||
/// </summary>
|
||||
/// <param name="request">The full wizard payload.</param>
|
||||
/// <returns>The created Tournament, including its divisions.</returns>
|
||||
Task<Tournament> CreateFullTournamentAsync(CreateFullTournamentRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Adds one division to an already-existing tournament, in a single transaction.
|
||||
/// </summary>
|
||||
/// <param name="tournament">The already-loaded parent tournament.</param>
|
||||
/// <param name="divisionRequest">The division's structure, zone or cross-cup.</param>
|
||||
/// <returns>The created Division.</returns>
|
||||
Task<Division> AddFullDivisionAsync(Tournament tournament, CreateFullDivisionRequest divisionRequest);
|
||||
|
||||
Task<Tournament?> GetTournamentByIdAsync(Guid tournamentId);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a tournament's full cloneable structure — every division with its
|
||||
/// Stages and PlayoffMappings — for the tournament-cloning wizard-prefill
|
||||
/// flow (HU-cloning). Carries no instance data (teams, matches, rosters).
|
||||
/// </summary>
|
||||
/// <param name="tournamentId">The source tournament's GUID id.</param>
|
||||
/// <returns>The tournament with its Divisions graph loaded, or null if not found.</returns>
|
||||
Task<Tournament?> GetTournamentStructureAsync(Guid tournamentId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a Tournament by its id or its slug asynchronously, treating the value as an id when it parses as a GUID and otherwise looking it up as a slug.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The Tournament's GUID id or its slug.</param>
|
||||
/// <returns>The matching Tournament, or null if not found.</returns>
|
||||
Task<Tournament?> GetTournamentByIdOrSlugAsync(string idOrSlug);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a Tournament without changing the lifecycle status; use ChangeStatusAsync for that.
|
||||
/// </summary>
|
||||
/// <param name="tournamentEntity">The Tournament to update.</param>
|
||||
Task UpdateTournamentAsync(Tournament tournamentEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Moves a tournament to a new lifecycle status, enforcing the forward-only state machine.
|
||||
/// </summary>
|
||||
/// <param name="tournamentId">The id of the tournament to transition.</param>
|
||||
/// <param name="newStatus">The target lifecycle status.</param>
|
||||
/// <exception cref="System.Collections.Generic.KeyNotFoundException">No tournament exists with the given id.</exception>
|
||||
/// <exception cref="System.InvalidOperationException">The requested transition is not allowed by the state machine.</exception>
|
||||
Task ChangeStatusAsync(Guid tournamentId, TournamentStatus newStatus);
|
||||
|
||||
/// <summary>
|
||||
/// Reports whether a tournament can be COMPLETED once started, and lists the blocking issues when it cannot.
|
||||
/// </summary>
|
||||
/// <param name="tournamentId">The id of the tournament to evaluate.</param>
|
||||
/// <returns>The completability report, with CanStart and Issues.</returns>
|
||||
/// <exception cref="System.Collections.Generic.KeyNotFoundException">No tournament exists with the given id.</exception>
|
||||
Task<TournamentCompletabilityResponse> GetCompletabilityAsync(Guid tournamentId);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a tournament, blocked once it has started or has any finished match, so competitive history is never silently destroyed.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the Tournament to delete.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the tournament has already started or has any finished match.
|
||||
/// </exception>
|
||||
Task DeleteTournamentAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves tournaments with pagination and filtering asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filtering and pagination request.</param>
|
||||
/// <returns>A paginated response containing the tournaments.</returns>
|
||||
Task<PaginatedResponse<Tournament>> GetAllTournamentsAsync(GetTournamentsFilteredRequest filter);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.User.Request;
|
||||
using Application.DTOs.User.Response;
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// CRUD operations on Identity users, with role-based access enforcement.
|
||||
/// </summary>
|
||||
public interface IUserManagementService
|
||||
{
|
||||
/// <summary>
|
||||
/// ADMIN sees all users; OWNER sees only their own subordinates; others get 403.
|
||||
/// </summary>
|
||||
Task<PaginatedResponse<UserResponse>> GetAllAsync(
|
||||
string callerRole, Guid callerId,
|
||||
UserFilteredRequest filter,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// ADMIN → any user. OWNER → self or their subordinates. Others → self only.
|
||||
/// </summary>
|
||||
Task<UserResponse> GetByIdAsync(
|
||||
string callerRole, Guid callerId, Guid userId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Updates profile fields including username, email, and phone, where ADMIN can update any user, OWNER can update self or their subordinates, and others only themselves.
|
||||
/// </summary>
|
||||
Task<UserResponse> UpdateAsync(
|
||||
string callerRole, Guid callerId, Guid userId,
|
||||
UpdateUserRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Changes a user's password, self-service or privileged, where ADMIN can change any user, OWNER can change self or their subordinates, and others only themselves.
|
||||
/// </summary>
|
||||
Task ChangePasswordAsync(
|
||||
string callerRole, Guid callerId, Guid userId,
|
||||
ChangePasswordRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Forces a password reset by generating a temporary password, where ADMIN can reset any user and OWNER only their own subordinates; others get 403.
|
||||
/// </summary>
|
||||
Task<ResetPasswordResponse> ResetPasswordAsync(
|
||||
string callerRole, Guid callerId, Guid userId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// ADMIN → any user. OWNER → their subordinates only. Others → 403.
|
||||
/// </summary>
|
||||
Task DeleteAsync(
|
||||
string callerRole, Guid callerId, Guid userId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Activates or deactivates a user account via Identity lockout, where ADMIN can act on any user and OWNER only their subordinates; others get 403.
|
||||
/// </summary>
|
||||
Task<UserResponse> SetActiveAsync(
|
||||
string callerRole, Guid callerId, Guid userId, bool isActive,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Services;
|
||||
|
||||
public interface IVenueService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a venue and generates its unique slug from the name.
|
||||
/// </summary>
|
||||
/// <param name="venueEntity">The Venue entity to create.</param>
|
||||
/// <returns>The created Venue.</returns>
|
||||
Task<Venue> CreateVenueAsync(Venue venueEntity);
|
||||
|
||||
Task<Venue?> GetVenueByIdAsync(Guid venueId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a Venue by its id or its slug asynchronously, treating the value as an id when it parses as a GUID and otherwise looking it up as a slug.
|
||||
/// </summary>
|
||||
/// <param name="idOrSlug">The venue's GUID id or its slug.</param>
|
||||
/// <returns>The matching venue, or null if not found.</returns>
|
||||
Task<Venue?> GetVenueByIdOrSlugAsync(string idOrSlug);
|
||||
|
||||
Task UpdateVenueAsync(Venue venueEntity);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a venue, blocked while any match still references it, so a match is never left without a venue.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the venue to delete.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the venue is referenced by one or more matches.
|
||||
/// </exception>
|
||||
Task DeleteVenueAsync(Guid id);
|
||||
|
||||
Task<IEnumerable<Venue>> GetAllVenuesAsync();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Interfaces.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Storage boundary for player medical-record files, PDF.
|
||||
/// </summary>
|
||||
public interface IMedicalRecordStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores a medical-record file for a player's team registration and returns the storage reference to persist on the registration.
|
||||
/// </summary>
|
||||
/// <param name="teamId">The team the registration belongs to.</param>
|
||||
/// <param name="playerId">The player the record belongs to.</param>
|
||||
/// <param name="fileName">The original file name, its extension is preserved.</param>
|
||||
/// <param name="content">The file content stream.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The stored object path used as the persisted file reference.</returns>
|
||||
Task<string> StoreAsync(Guid teamId, Guid playerId, string fileName, Stream content, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the raw bytes of a previously stored medical-record object.
|
||||
/// </summary>
|
||||
/// <param name="objectPath">
|
||||
/// The stored object path returned by StoreAsync and persisted
|
||||
/// on the season registration as its file reference.
|
||||
/// </param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The raw file content.</returns>
|
||||
Task<byte[]> DownloadAsync(string objectPath, CancellationToken ct = default);
|
||||
}
|
||||
Reference in New Issue
Block a user