Sube carpetas API, API.Tests, Application y Domain del Backend

This commit is contained in:
FrancoRu
2026-09-18 13:35:59 -03:00
parent bbb09554bb
commit 2eb4eeef03
572 changed files with 56405 additions and 0 deletions
@@ -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();
}