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,9 @@
namespace Domain.Constants;
/// <summary>
/// Identifies audit-trail entries created by an automated process rather than an authenticated user.
/// </summary>
public static class AuditConstants
{
public const string SystemUser = "System";
}
@@ -0,0 +1,17 @@
namespace Domain.Constants;
/// <summary>
/// Regulation defaults applied to matches when no explicit score is entered.
/// </summary>
public static class MatchDefaults
{
/// <summary>
/// Default score awarded to the present team on a walkover.
/// </summary>
public const int WalkOverWinnerScore = 20;
/// <summary>
/// Score assigned to the absent team on a walkover.
/// </summary>
public const int WalkOverLoserScore = 0;
}
+7
View File
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
@@ -0,0 +1,17 @@
namespace Domain.Entities.Models;
/// <summary>
/// The point-deduction summary attached to a Position row when a team carries one or more disciplinary deductions in its division.
/// </summary>
public class AppliedPointDeduction
{
/// <summary>
/// The total table points subtracted from the team, always positive.
/// </summary>
public required int Points { get; set; }
/// <summary>
/// The combined disciplinary reasons, joined when a team has several deductions.
/// </summary>
public required string Reason { get; set; }
}
@@ -0,0 +1,41 @@
using Domain.Enums;
namespace Domain.Entities.Models;
/// <summary>
/// An immutable audit-trail entry recording a sensitive action, since the shared owner and admin accounts are otherwise indistinguishable.
/// </summary>
public class AuditLog : EntityBase
{
/// <summary>
/// The sensitive action that was performed.
/// </summary>
public required AuditAction Action { get; set; }
/// <summary>
/// Who performed the action — the authenticated caller's email, or AuditConstants.SystemUser for an automated process with no HTTP identity.
/// </summary>
public required string Actor { get; set; }
/// <summary>
/// The kind of entity the action targeted, or null when the action is not tied to a single entity.
/// </summary>
public string? TargetType { get; set; }
/// <summary>
/// Identifier of the targeted entity, when applicable.
/// </summary>
public string? TargetId { get; set; }
/// <summary>
/// The target's human-readable name captured at write time, since the target may later be renamed or deleted.
/// </summary>
public string? TargetName { get; set; }
/// <summary>
/// Free-form human-readable context describing the action in detail.
/// </summary>
public string? Detail { get; set; }
// Timestamp is EntityBase.DateCreated in UTC.
}
@@ -0,0 +1,24 @@
using Domain.Enums;
namespace Domain.Entities.Models;
/// <summary>
/// Durable, queryable per-backup record, the source of truth for the admin backup listing.
/// </summary>
public class BackupRecord : EntityBase
{
/// <summary>
/// Where the dump is stored, as an object key or file path understood by the IBackupStorage adapter that persisted it.
/// </summary>
public required string StoragePath { get; set; }
/// <summary>
/// Size, in bytes, of the stored dump.
/// </summary>
public long SizeBytes { get; set; }
/// <summary>
/// Whether this backup was triggered manually by an Admin or created by the scheduled backup job.
/// </summary>
public required BackupOrigin Origin { get; set; }
}
@@ -0,0 +1,21 @@
namespace Domain.Entities.Models;
public class BlogPost : EntityBase
{
public required string Author { get; set; }
public required string Title { get; set; }
/// <summary>
/// The unique, URL-friendly identifier used in public blog post links, generated once from the title and never changed afterward.
/// </summary>
public required string Slug { get; set; }
public int Views { get; set; } = 0;
public string? PhotoUrl { get; set; }
public required string MarkdownText { get; set; }
/// <summary>
/// Publication state: when false the post is a draft, hidden from public views and visible only to Admin or Owner callers.
/// </summary>
public bool IsPublished { get; set; } = true;
}
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
namespace Domain.Entities.Models;
/// <summary>
/// A club is the stable identity that persists across seasons, unlike a Team, which is a per-season registration record.
/// </summary>
public class Club : EntityBase
{
/// <summary>
/// The club's stable display name.
/// </summary>
public required string Name { get; set; }
/// <summary>
/// The unique, URL-friendly identifier used in public club links, generated once from the name and never changed afterward.
/// </summary>
public required string Slug { get; set; }
/// <summary>
/// Optional club crest or logo URL, independent of any season team logo.
/// </summary>
public string? LogoUrl { get; set; }
/// <summary>
/// Every per-season Team that belongs to this club, across every tournament.
/// </summary>
public virtual ICollection<Team> Teams { get; set; } = [];
/// <summary>
/// Optional link to the parent institution this club is a squad of, independent of tournament category. Flat, one level deep — a club with its own ChildClubs cannot itself have a ParentClubId.
/// </summary>
public Guid? ParentClubId { get; set; }
public Club? ParentClub { get; set; }
/// <summary>
/// Other clubs (squads) that link to this club as their parent institution.
/// </summary>
public virtual ICollection<Club> ChildClubs { get; set; } = [];
}
@@ -0,0 +1,56 @@
using Domain.Enums;
using System;
using System.Collections.Generic;
namespace Domain.Entities.Models;
public class Division : EntityBase
{
public required string Name { get; set; }
/// <summary>
/// The unique, URL-friendly identifier used in public division links, generated once from the name and never changed afterward.
/// </summary>
public required string Slug { get; set; }
public bool IsFinished { get; set; } = false;
public required Tournament Tournament { get; set; }
public Guid TournamentId { get; set; }
/// <summary>
/// Competitive gender category of the division, which must match the parent Tournament's Category.
/// </summary>
public TournamentCategory Category { get; set; } = TournamentCategory.Masculine;
public virtual required ICollection<Stage> Stages { get; set; }
/// <summary>
/// Marks a division that intentionally draws teams from across every other division in the tournament, rather than a team's single competitive tier.
/// </summary>
public bool IsCrossDivisionCup { get; set; } = false;
/// <summary>
/// Points awarded for a win when building this division's standings, configurable per division. Defaults to 2.
/// </summary>
public int PointsForWin { get; set; } = 2;
/// <summary>
/// Points awarded for a loss when building this division's standings. Defaults to 1.
/// </summary>
public int PointsForLoss { get; set; } = 1;
/// <summary>
/// How many teams qualify to the bracket from each internal group of a multi-group cross-division cup. Defaults to 1.
/// </summary>
public int QualifiersPerGroup { get; set; } = 1;
/// <summary>
/// Maps this division's final standings position ranges to playoff destinations.
/// </summary>
public virtual ICollection<DivisionPlayoffMapping> PlayoffMappings { get; set; } = [];
public virtual ICollection<DivisionTeamRegistration> DivisionTeamRegistrations { get; set; } = [];
}
@@ -0,0 +1,34 @@
using System;
namespace Domain.Entities.Models;
/// <summary>
/// Maps a contiguous range of final group-stage standings positions in a division to a playoff destination.
/// </summary>
public class DivisionPlayoffMapping : EntityBase
{
/// <summary>
/// The division whose standings this mapping applies to.
/// </summary>
public Guid DivisionId { get; set; }
/// <summary>
/// The division whose standings this mapping applies to.
/// </summary>
public Division? Division { get; set; }
/// <summary>
/// First, 1-based inclusive standings position in the range.
/// </summary>
public required int FromPosition { get; set; }
/// <summary>
/// Last, 1-based inclusive standings position in the range, greater than or equal to FromPosition.
/// </summary>
public required int ToPosition { get; set; }
/// <summary>
/// The playoff destination the teams in this range qualify for, matching the BracketName of that cup's elimination stages.
/// </summary>
public required string Destination { get; set; }
}
@@ -0,0 +1,15 @@
using System;
namespace Domain.Entities.Models;
/// <summary>
/// The authoritative record that a team is enrolled in a division, independent of any stage placement.
/// </summary>
public class DivisionTeamRegistration : EntityBase
{
public required Guid TeamId { get; set; }
public Team? Team { get; set; }
public required Guid DivisionId { get; set; }
public Division? Division { get; set; }
}
@@ -0,0 +1,23 @@
using Domain.Constants;
using System;
namespace Domain.Entities.Models;
/// <summary>
/// Base class for all domain entities, centralizing the identifier and the created and updated audit fields shared by every entity.
/// </summary>
public abstract class EntityBase
{
public Guid Id { get; set; } = Guid.Empty;
public DateTime DateCreated { get; set; } = DateTime.UtcNow;
/// <summary>
/// Null until the entity is modified after creation.
/// </summary>
public DateTime? DateUpdated { get; set; }
public required string CreatedBy { get; set; } = AuditConstants.SystemUser;
public string? UpdatedBy { get; set; }
}
@@ -0,0 +1,80 @@
using Domain.Enums;
using System;
using System.Collections.Generic;
namespace Domain.Entities.Models;
public class Match : EntityBase
{
public DateTime MatchDate { get; set; }
/// <summary>
/// The 1-based matchday this match belongs to within its stage — the canonical fixture grouping key, not the calendar date.
/// </summary>
public int? Round { get; set; }
public required MatchType Type { get; set; }
/// <summary>
/// The unique, URL-friendly identifier used in public match links, generated once from the team names and match date and never changed afterward.
/// </summary>
public required string Slug { get; set; }
public Team? HomeTeam { get; set; }
public Guid? HomeTeamId { get; set; }
public Team? VisitorTeam { get; set; }
public Guid? VisitorTeamId { get; set; }
/// <summary>
/// Null until the match result is loaded.
/// </summary>
public int? HomeScore { get; set; }
/// <summary>
/// Null until the match result is loaded.
/// </summary>
public int? VisitorScore { get; set; }
public required bool IsFinished { get; set; }
/// <summary>
/// Whether the match was decided in overtime, purely informational and with no effect on scoring or standings.
/// </summary>
public bool WentToOvertime { get; set; }
/// <summary>
/// The match's result lifecycle state: Scheduled, Played, Suspended, or WalkOver.
/// </summary>
public MatchStatus Status { get; set; } = MatchStatus.Scheduled;
public Team? WinningTeam { get; set; }
public Guid? WinningTeamId { get; set; }
public Stage Stage { get; set; } = default!;
public Guid? VenueId { get; set; }
public Venue? Venue { get; set; }
public Guid StageId { get; set; }
public virtual ICollection<PlayerStatistic> PlayerStatistics { get; set; } = [];
public virtual ICollection<Scorer> Scorers { get; set; } = [];
/// <summary>
/// The best-of-N series this game belongs to, when the stage's BestOf is greater than 1, null for single-game rounds.
/// </summary>
public Guid? SeriesId { get; set; }
public MatchSeries? Series { get; set; }
/// <summary>
/// The 1-based game number within its series, null when the match does not belong to a series.
/// </summary>
public int? GameNumber { get; set; }
}
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
namespace Domain.Entities.Models;
/// <summary>
/// A best-of-N playoff series between two teams at one bracket round, grouping the individual Match games that make it up.
/// </summary>
public class MatchSeries : EntityBase
{
public required Guid StageId { get; set; }
public Stage? Stage { get; set; }
public required Guid HomeTeamId { get; set; }
public Team? HomeTeam { get; set; }
public required Guid VisitorTeamId { get; set; }
public Team? VisitorTeam { get; set; }
/// <summary>
/// Number of games in this series, copied from the stage at creation time so a later change to Stage.BestOf never retroactively changes it.
/// </summary>
public required int BestOf { get; set; }
/// <summary>
/// Set once one team has won the majority of the series' games.
/// </summary>
public Guid? WinningTeamId { get; set; }
public Team? WinningTeam { get; set; }
public virtual ICollection<Match> Matches { get; set; } = [];
}
@@ -0,0 +1,90 @@
using Domain.Enums;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace Domain.Entities.Models;
public class Player : EntityBase
{
public required string FirstName { get; set; }
public string? SecondName { get; set; }
public required string LastName { get; set; }
/// <summary>
/// The unique, URL-friendly identifier used in public player links, generated once from the player's full name and never changed afterward.
/// </summary>
public required string Slug { get; set; }
/// <summary>
/// The single canonical source string every slug is derived from, in the player's raw casing and with no document number.
/// </summary>
/// <param name="lastName">The player's last name.</param>
/// <param name="firstName">The player's first name.</param>
/// <param name="secondName">The optional second given name; blank is treated as absent.</param>
/// <returns>The space-joined name source, formed by joining last name, first name, and second name with spaces.</returns>
public static string BuildSlugSource(string lastName, string firstName, string? secondName)
{
if (string.IsNullOrWhiteSpace(secondName))
{
return $"{lastName} {firstName}";
}
return $"{lastName} {firstName} {secondName}";
}
/// <summary>
/// Computed and not persisted: the canonical slug source for this player.
/// </summary>
public string SlugSource => BuildSlugSource(LastName, FirstName, SecondName);
/// <summary>
/// Computed and not persisted to the database: display name with the last name upper-cased.
/// </summary>
public string FullName => BuildSlugSource(LastName.ToUpper(), FirstName, SecondName);
public required string DocumentNumber { get; set; }
public required bool IsSanctioned { get; set; } = false;
public string? PhoneNumber { get; set; }
public required DateTime BirthDate { get; set; }
public required string SocialSecurity { get; set; }
/// <summary>
/// Denormalized convenience pointer to the player's current team, always kept in sync with their latest PlayerTeamRegistration.
/// </summary>
public required Team Team { get; set; }
public Guid TeamId { get; set; }
public virtual ICollection<Scorer> Scorers { get; set; } = [];
/// <summary>
/// Every season this player was registered to a team, the source of truth for roster membership.
/// </summary>
public virtual ICollection<PlayerTeamRegistration> PlayerTeamRegistrations { get; set; } = [];
/// <summary>
/// Transient and not persisted: the medical-record eligibility status of this player for the season roster currently being viewed.
/// </summary>
[NotMapped]
public MedicalRecordStatus? MedicalRecordStatus { get; set; }
/// <summary>
/// Transient and not persisted: whether the season roster currently being viewed set a real, non-legacy stored medical-record file reference.
/// </summary>
[NotMapped]
public bool HasMedicalRecordFile { get; set; }
/// <summary>
/// Transient and not persisted: whether the player is habilitado for the season roster currently being viewed.
/// </summary>
[NotMapped]
public bool IsHabilitado =>
MedicalRecordStatus == Domain.Enums.MedicalRecordStatus.Approved && HasMedicalRecordFile;
/// <summary>
/// Transient and not persisted: the player's jersey number for the season roster currently being viewed.
/// </summary>
[NotMapped]
public int? JerseyNumber { get; set; }
}
@@ -0,0 +1,51 @@
using Domain.Enums;
using System;
namespace Domain.Entities.Models;
public class PlayerSanction : EntityBase
{
/// <summary>
/// The sanction's length expressed in fechas, not calendar days: a value of N means the subject sits out the next N rounds.
/// </summary>
public required int Duration { get; set; }
public required DateTime IssuedDate { get; set; }
public required string Description { get; set; }
/// <summary>
/// What kind of subject this sanction targets: a player, a team, or a staff member. Defaults to Player.
/// </summary>
public SanctionSubjectType SubjectType { get; set; } = SanctionSubjectType.Player;
/// <summary>
/// The sanctioned player, only set and required when SubjectType is Player.
/// </summary>
public Player? Player { get; set; }
public Guid? PlayerId { get; set; }
/// <summary>
/// The sanctioned team, only set when SubjectType is Team.
/// </summary>
public Team? Team { get; set; }
public Guid? TeamId { get; set; }
/// <summary>
/// The sanctioned staff member's name, only set when SubjectType is Staff.
/// </summary>
public string? StaffName { get; set; }
public required Match Match { get; set; }
public Guid MatchId { get; set; }
/// <summary>
/// The unique, URL-friendly identifier used in public and admin sanction links, generated once from the subject's name and the issued date.
/// </summary>
public required string Slug { get; set; }
public SanctionAppealStatus AppealStatus { get; set; } = SanctionAppealStatus.None;
public string? AppealReason { get; set; }
public DateTime? AppealDate { get; set; }
public string? AppealResolution { get; set; }
public DateTime? AppealResolvedDate { get; set; }
}
@@ -0,0 +1,15 @@
using Domain.Enums;
using System;
namespace Domain.Entities.Models;
public class PlayerStatistic : EntityBase
{
public required int Value { get; set; }
public StatisticType Type { get; set; } = StatisticType.Points;
public Match? Match { get; set; }
public required Guid MatchId { get; set; }
public Player? Player { get; set; }
public required Guid PlayerId { get; set; }
}
@@ -0,0 +1,73 @@
using Domain.Enums;
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace Domain.Entities.Models;
/// <summary>
/// Links a Player to a Team for exactly one season, the source of truth for roster membership, unlike the denormalized Player.TeamId pointer.
/// </summary>
public class PlayerTeamRegistration : EntityBase
{
public required Guid PlayerId { get; set; }
public Player? Player { get; set; }
public required Guid TeamId { get; set; }
public Team? Team { get; set; }
/// <summary>
/// The season this registration belongs to, captured at registration time from the Team's TournamentId at that moment.
/// </summary>
public required Guid TournamentId { get; set; }
public Tournament? Tournament { get; set; }
/// <summary>
/// The player's jersey number for this season registration, null when no number has been assigned yet.
/// </summary>
public int? JerseyNumber { get; set; }
/// <summary>
/// Medical-record eligibility status for this season registration, defaulting to Pending.
/// </summary>
public MedicalRecordStatus MedicalRecordStatus { get; set; } = MedicalRecordStatus.Pending;
/// <summary>
/// Storage reference of the uploaded medical-record file for this season, null until a file is uploaded.
/// </summary>
public string? MedicalRecordFileUrl { get; set; }
/// <summary>
/// Original file name of the uploaded medical-record file, kept for display.
/// </summary>
public string? MedicalRecordFileName { get; set; }
/// <summary>
/// Optional reason recorded when the medical record is rejected.
/// </summary>
public string? MedicalRecordReviewReason { get; set; }
/// <summary>
/// When the medical record was last approved or rejected, null while still Pending.
/// </summary>
public DateTime? MedicalRecordReviewedAt { get; set; }
/// <summary>
/// Refs written before the private-bucket relocation lived under this prefix inside public-images and no longer resolve.
/// </summary>
public const string LegacyReferencePrefix = "medical-records/";
/// <summary>
/// Whether fileReference is a real, resolvable stored file reference: non-null, non-whitespace, and not a legacy prefix ref.
/// </summary>
public static bool IsStoredReference(string? fileReference) =>
!string.IsNullOrWhiteSpace(fileReference)
&& !fileReference.StartsWith(LegacyReferencePrefix, StringComparison.Ordinal);
/// <summary>
/// Whether this registration's player is habilitado for this season: Approved status and a real stored file reference.
/// </summary>
[NotMapped]
public bool IsHabilitado =>
MedicalRecordStatus == Enums.MedicalRecordStatus.Approved && IsStoredReference(MedicalRecordFileUrl);
}
@@ -0,0 +1,53 @@
using Domain.Enums;
using System;
namespace Domain.Entities.Models;
/// <summary>
/// Represents the position of a team in a division, including its match statistics, used in the service layer.
/// </summary>
public class Position
{
public required Guid TeamId { get; set; }
public required string TeamName { get; set; }
public required string LogoUrl { get; set; }
public required int MatchesPlayed { get; set; }
public required int Wins { get; set; }
public required int Losses { get; set; }
/// <summary>
/// Basketball score totaled across all matches, distinct from the standings score in Points.
/// </summary>
public required int PointsFor { get; set; }
/// <summary>
/// Basketball score conceded, totaled across all matches, the counterpart to PointsFor.
/// </summary>
public required int PointsAgainst { get; set; }
/// <summary>
/// The point difference, PointsFor minus PointsAgainst, for the team.
/// </summary>
public required int PointsDifference { get; set; }
/// <summary>
/// The total table points accumulated by the team, calculated from the division's points-per-win and points-per-loss configuration.
/// </summary>
public required int Points { get; set; }
/// <summary>
/// The tiebreaker criterion that separated this team from the team ranked immediately above it.
/// </summary>
public TiebreakerCriterion? ResolvedBy { get; set; }
/// <summary>
/// The disciplinary point deduction applied to this team, null when the team has no deduction.
/// </summary>
public AppliedPointDeduction? PointDeduction { get; set; }
}
@@ -0,0 +1,16 @@
using System;
namespace Domain.Entities.Models;
public class Scorer : EntityBase
{
public required Guid PlayerId { get; set; }
public Player? Player { get; set; }
public required int Points { get; set; }
public required Guid MatchId { get; set; }
public Match? Match { get; set; }
}
@@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace Domain.Entities.Models;
/// <summary>
/// Represents a season in the Club12 application, a top-level grouping that gathers several tournaments played in the same competitive period.
/// </summary>
public class Season : EntityBase
{
/// <summary>
/// The display name of the season, in the form "Temporada XXVII".
/// </summary>
public required string Name { get; set; }
/// <summary>
/// The unique, URL-friendly identifier used in public season links, generated once from the name and never changed afterward.
/// </summary>
public required string Slug { get; set; }
/// <summary>
/// The optional calendar year the season is played in.
/// </summary>
public int? Year { get; set; }
/// <summary>
/// The tournaments grouped under this season, empty until tournaments are assigned since membership is optional.
/// </summary>
public virtual ICollection<Tournament> Tournaments { get; set; } = [];
}
@@ -0,0 +1,49 @@
using Domain.Enums;
using System;
using System.Collections.Generic;
namespace Domain.Entities.Models;
public class Stage : EntityBase
{
public required string Name { get; set; }
/// <summary>
/// The unique, URL-friendly identifier used in public stage links, generated once from the name and never changed afterward.
/// </summary>
public required string Slug { get; set; }
public string? Description { get; set; }
public required StageType StageType { get; set; }
public required bool IsActive { get; set; }
public bool IsElimination { get; set; }
public required DateTime StartDate { get; set; }
public required DateTime EndDate { get; set; }
public required Guid DivisionId { get; set; }
public required Division Division { get; set; }
public virtual required ICollection<Match> Matches { get; set; } = [];
public int Order { get; set; }
public virtual ICollection<StageTeamMatch> StageTeamMatches { get; set; } = [];
/// <summary>
/// Groups multiple parallel elimination brackets under the same division, null when the stage belongs to the division's default bracket.
/// </summary>
public string? BracketName { get; set; }
/// <summary>
/// Number of games in a series between two teams at this round, defaulting to 1 for a single match deciding the round.
/// </summary>
public int BestOf { get; set; } = 1;
public virtual ICollection<MatchSeries> MatchSeries { get; set; } = [];
/// <summary>
/// How many times each pair of teams plays within this group stage, defaulting to 1 for a single round-robin.
/// </summary>
public int RoundRobinLegs { get; set; } = 1;
/// <summary>
/// When this bracket's seeding draw was committed, null until a draw runs.
/// </summary>
public DateTime? DrawnAt { get; set; }
}
@@ -0,0 +1,11 @@
using System;
namespace Domain.Entities.Models;
public class StageTeamMatch : EntityBase
{
public required Guid StageId { get; set; }
public Stage? Stage { get; set; }
public required Guid TeamId { get; set; }
public Team? Team { get; set; }
}
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
namespace Domain.Entities.Models;
public class Team : EntityBase
{
public required string Name { get; set; }
/// <summary>
/// The unique, URL-friendly identifier used in public team links, generated once from the name and never changed afterward.
/// </summary>
public required string Slug { get; set; }
public required string ThreeLetterCode { get; set; }
public required string LogoUrl { get; set; }
public required string ShirtColor { get; set; }
/// <summary>
/// The jersey kit pattern applied over the primary ShirtColor, defaulting to solid.
/// </summary>
public string JerseyStyle { get; set; } = "solid";
/// <summary>
/// Optional secondary hex color used for the jersey pattern or trim, null when the kit is a plain solid with no accent.
/// </summary>
public string? ShirtSecondaryColor { get; set; }
/// <summary>
/// Optional third hex color, used only by tri-color kit templates as a second accent alongside ShirtSecondaryColor.
/// </summary>
public string? ShirtTertiaryColor { get; set; }
public Tournament? Tournament { get; set; }
public Guid? TournamentId { get; set; }
/// <summary>
/// Optional link to the team's stable cross-season identity, purely additive and never breaking existing per-season Team behavior.
/// </summary>
public Guid? ClubId { get; set; }
public Club? Club { get; set; }
/// <summary>
/// Players whose denormalized current team pointer points here, not season-scoped.
/// </summary>
public virtual required ICollection<Player> Players { get; set; } = [];
public virtual ICollection<StageTeamMatch> StageTeamMatches { get; set; } = [];
/// <summary>
/// Every player ever registered to this team, across every season it has belonged to, the source of truth for roster membership.
/// </summary>
public virtual ICollection<PlayerTeamRegistration> PlayerTeamRegistrations { get; set; } = [];
/// <summary>
/// Every tournament this team has ever been registered to, across every season, independent of the current TournamentId pointer.
/// </summary>
public virtual ICollection<TeamTournamentRegistration> TeamTournamentRegistrations { get; set; } = [];
public virtual ICollection<DivisionTeamRegistration> DivisionTeamRegistrations { get; set; } = [];
}
@@ -0,0 +1,39 @@
using System;
namespace Domain.Entities.Models;
/// <summary>
/// A disciplinary points penalty applied by an admin to a team within a division, subtracted from that team's standings total.
/// </summary>
public class TeamPointDeduction : EntityBase
{
/// <summary>
/// The division whose standings the penalty affects.
/// </summary>
public Guid DivisionId { get; set; }
/// <summary>
/// The division navigation, optional so the entity can be built from an id alone; deleting the division cascades its deductions away.
/// </summary>
public Division? Division { get; set; }
/// <summary>
/// The penalised team.
/// </summary>
public Guid TeamId { get; set; }
/// <summary>
/// The team navigation, optional so the entity can be built from an id alone; loaded when the caller needs the team's name.
/// </summary>
public Team? Team { get; set; }
/// <summary>
/// The positive amount of table points to subtract from the team's total.
/// </summary>
public required int Points { get; set; }
/// <summary>
/// The disciplinary reason for the deduction, shown next to the penalised team in the standings.
/// </summary>
public required string Reason { get; set; }
}
@@ -0,0 +1,41 @@
using Domain.Enums;
using System;
namespace Domain.Entities.Models;
/// <summary>
/// A member of a team's technical staff, scoped to one team within one tournament, mirroring the season scoping used by PlayerTeamRegistration.
/// </summary>
public class TeamStaff : EntityBase
{
/// <summary>
/// The team this staff member belongs to.
/// </summary>
public Guid TeamId { get; set; }
/// <summary>
/// The team navigation, optional so the entity can be built from an id alone; loaded when the caller needs the team's name.
/// </summary>
public Team? Team { get; set; }
/// <summary>
/// The season this staff membership belongs to.
/// </summary>
public Guid TournamentId { get; set; }
/// <summary>
/// The tournament navigation, optional so the entity can be built from an id alone.
/// </summary>
public Tournament? Tournament { get; set; }
/// <summary>
/// The staff member's full name.
/// </summary>
public required string FullName { get; set; }
/// <summary>
/// The role this person holds on the team's technical staff.
/// </summary>
public required TeamStaffRole Role { get; set; }
}
@@ -0,0 +1,18 @@
using System;
namespace Domain.Entities.Models;
/// <summary>
/// Links a Team to a Tournament for exactly one season, the source of truth for a team's participation history, unlike the denormalized Team.TournamentId pointer.
/// </summary>
public class TeamTournamentRegistration : EntityBase
{
public required Guid TeamId { get; set; }
public Team? Team { get; set; }
/// <summary>
/// The season this registration belongs to, captured at registration time.
/// </summary>
public required Guid TournamentId { get; set; }
public Tournament? Tournament { get; set; }
}
@@ -0,0 +1,46 @@
using Domain.Enums;
using System;
using System.Collections.Generic;
namespace Domain.Entities.Models;
public class Tournament : EntityBase
{
public required string Description { get; set; }
public required string Name { get; set; }
/// <summary>
/// The unique, URL-friendly identifier used in public tournament links, generated once from the name and never changed afterward.
/// </summary>
public required string Slug { get; set; }
/// <summary>
/// The deadline for team registrations, which must be earlier than the tournament start date.
/// </summary>
public required DateTime TeamRegistrationDeadline { get; set; }
public required DateTime StartDate { get; set; }
public TournamentStatus Status { get; set; } = TournamentStatus.Scheduled;
/// <summary>
/// Competitive gender category of the tournament, the source of truth for the no-mixing invariant across its divisions.
/// </summary>
public TournamentCategory Category { get; set; } = TournamentCategory.Masculine;
/// <summary>
/// The optional id of the season this tournament belongs to, purely additive and never affecting the tournament's own Category.
/// </summary>
public Guid? SeasonId { get; set; }
/// <summary>
/// The season this tournament belongs to, or null when it is not grouped under any season.
/// </summary>
public virtual Season? Season { get; set; }
public virtual required ICollection<Division> Divisions { get; set; }
public virtual required ICollection<Team> Teams { get; set; }
}
@@ -0,0 +1,24 @@
namespace Domain.Entities.Models;
public class Venue : EntityBase
{
public required string Name { get; set; }
/// <summary>
/// The unique, URL-friendly identifier used in public venue links, generated once from the name and never changed afterward.
/// </summary>
public required string Slug { get; set; }
public required string Address { get; set; }
public string? PhotoUrl { get; set; }
/// <summary>
/// Optional geographic latitude of the venue, used to build a public map link, null when the venue has no coordinates yet.
/// </summary>
public double? Latitude { get; set; }
/// <summary>
/// Optional geographic longitude of the venue, used to build a public map link, null when the venue has no coordinates yet.
/// </summary>
public double? Longitude { get; set; }
}
@@ -0,0 +1,32 @@
namespace Domain.Enums;
/// <summary>
/// The sensitive, auditable actions tracked for traceability, persisted as the enum name so the trail survives numeric reordering.
/// </summary>
public enum AuditAction
{
/// <summary>
/// Full tournament-domain data wipe, triggered by DataMaintenanceService.
/// </summary>
DataWipe,
/// <summary>
/// Database restore from a backup, triggered by BackupRestoreService.
/// </summary>
BackupRestore,
/// <summary>
/// A tournament lifecycle status change, triggered by TournamentService.
/// </summary>
TournamentStatusChange,
/// <summary>
/// An admin-triggered password reset, from user management.
/// </summary>
PasswordReset,
/// <summary>
/// A bracket seeding draw, initial or re-draw, recorded for transparency.
/// </summary>
PlayoffDraw,
}
@@ -0,0 +1,17 @@
namespace Domain.Enums;
/// <summary>
/// How a catalogued database backup was created.
/// </summary>
public enum BackupOrigin
{
/// <summary>
/// Triggered on-demand by an Admin from the Data Administration panel.
/// </summary>
Manual,
/// <summary>
/// Created automatically by the scheduled DatabaseBackupHostedService.
/// </summary>
Job
}
+10
View File
@@ -0,0 +1,10 @@
namespace Domain.Enums;
/// <summary>
/// How a playoffs-only bracket's first-round team order is produced: shuffled server-side or set by an admin.
/// </summary>
public enum DrawMode
{
Random,
Manual,
}
@@ -0,0 +1,32 @@
namespace Domain.Enums;
/// <summary>
/// The lifecycle state of a match's result, distinct from the boolean Match.IsFinished flag.
/// </summary>
public enum MatchStatus
{
/// <summary>
/// The default state: the fixture exists but no result has been loaded yet.
/// </summary>
Scheduled,
/// <summary>
/// The match was played and a decisive result was loaded.
/// </summary>
Played,
/// <summary>
/// The match was postponed or suspended and has no result.
/// </summary>
Suspended,
/// <summary>
/// One team did not show up, so the regulation default result was applied to the present team.
/// </summary>
WalkOver,
/// <summary>
/// The match will never be played because its tournament was canceled or force-closed while it was still pending.
/// </summary>
Canceled
}
+7
View File
@@ -0,0 +1,7 @@
namespace Domain.Enums;
public enum MatchType
{
Regular,
Playoff
}
@@ -0,0 +1,22 @@
namespace Domain.Enums;
/// <summary>
/// Medical-record eligibility status of a player's season registration, scoped per player, team, and tournament.
/// </summary>
public enum MedicalRecordStatus
{
/// <summary>
/// Default for a brand-new registration: no medical record uploaded yet, or uploaded but not yet reviewed.
/// </summary>
Pending,
/// <summary>
/// The owner or admin reviewed and approved the medical record, making the player habilitado for that team and tournament.
/// </summary>
Approved,
/// <summary>
/// The owner or admin rejected the medical record, usually with a reason, leaving the player not habilitado.
/// </summary>
Rejected,
}
+16
View File
@@ -0,0 +1,16 @@
namespace Domain.Enums;
/// <summary>
/// Compile-time string constants for every UserRoleType value, used in authorization attributes to avoid magic strings.
/// </summary>
public static class Roles
{
public const string Admin = nameof(UserRoleType.ADMIN);
public const string Owner = nameof(UserRoleType.OWNER);
public const string Guest = nameof(UserRoleType.GUEST);
/// <summary>
/// Every staff role that may operate the panel: the Owner account and the Admin IT account.
/// </summary>
public const string AdminOrOwner = $"{Admin},{Owner}";
}
@@ -0,0 +1,24 @@
namespace Domain.Enums;
public enum SanctionAppealStatus
{
/// <summary>
/// No appeal has been submitted.
/// </summary>
None,
/// <summary>
/// An appeal was submitted and is awaiting a decision.
/// </summary>
Pending,
/// <summary>
/// The appeal was accepted and the sanction is overturned.
/// </summary>
Accepted,
/// <summary>
/// The appeal was rejected and the sanction stands.
/// </summary>
Rejected
}
@@ -0,0 +1,22 @@
namespace Domain.Enums;
/// <summary>
/// The kind of subject a PlayerSanction is applied to: an individual player, a whole team, or a staff member.
/// </summary>
public enum SanctionSubjectType
{
/// <summary>
/// The sanction targets an individual player, identified by PlayerSanction.PlayerId.
/// </summary>
Player,
/// <summary>
/// The sanction targets a whole team as an institutional sanction, identified by PlayerSanction.TeamId.
/// </summary>
Team,
/// <summary>
/// The sanction targets a staff member, identified only by PlayerSanction.StaffName.
/// </summary>
Staff
}
+19
View File
@@ -0,0 +1,19 @@
namespace Domain.Enums;
public enum StageType
{
/// <summary>
/// Group stage, where teams play in a round-robin format.
/// </summary>
Group,
/// <summary>
/// Round of 16 stage.
/// </summary>
RoundOf16,
QuarterFinal,
SemiFinal,
ThirdPlace,
Final
}
@@ -0,0 +1,7 @@
namespace Domain.Enums;
public enum StatisticType
{
Points,
Assists
}
@@ -0,0 +1,17 @@
namespace Domain.Enums;
/// <summary>
/// The role a member of a team's technical staff holds for a given season.
/// </summary>
public enum TeamStaffRole
{
/// <summary>
/// Head coach.
/// </summary>
Coach,
/// <summary>
/// Assistant coach.
/// </summary>
AssistantCoach
}
@@ -0,0 +1,32 @@
namespace Domain.Enums;
/// <summary>
/// The group-stage tiebreaker criteria, in the exact priority order the club's rulebook applies them.
/// </summary>
public enum TiebreakerCriterion
{
/// <summary>
/// Table points awarded for wins and losses.
/// </summary>
Points,
/// <summary>
/// Games won across the whole zone.
/// </summary>
GamesWon,
/// <summary>
/// Points for minus points against, across the whole zone.
/// </summary>
PointsDifference,
/// <summary>
/// Head-to-head result among the tied teams.
/// </summary>
HeadToHead,
/// <summary>
/// Points difference considering only the games played among the tied teams.
/// </summary>
HeadToHeadPointsDifference,
}
@@ -0,0 +1,17 @@
namespace Domain.Enums;
/// <summary>
/// Competitive gender category of a tournament, living on the Tournament as the source of truth every division must share.
/// </summary>
public enum TournamentCategory
{
/// <summary>
/// Masculine competition, the default for existing or unspecified tournaments.
/// </summary>
Masculine = 0,
/// <summary>
/// Feminine competition, played as its own separate tournament.
/// </summary>
Feminine = 1
}
@@ -0,0 +1,23 @@
namespace Domain.Enums;
/// <summary>
/// Lifecycle status of a Tournament; see TournamentStatusTransitions for the allowed moves between these values.
/// </summary>
public enum TournamentStatus
{
/// <summary>
/// The tournament is scheduled but not yet open for team registrations.
/// </summary>
Scheduled,
OpenForRegistration,
/// <summary>
/// Registration has closed, freezing structural changes: the roster is fixed and teams are assigned to divisions.
/// </summary>
RegistrationClosed,
Ongoing,
Finished,
/// <summary>
/// The tournament has been canceled and will not take place.
/// </summary>
Canceled,
}
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Linq;
namespace Domain.Enums;
/// <summary>
/// Encodes the tournament lifecycle state machine, a linear happy path from Scheduled through Finished plus a Canceled escape from any non-terminal state.
/// </summary>
public static class TournamentStatusTransitions
{
/// <summary>
/// The set of states each status may move to next, an absent status meaning an invalid transition.
/// </summary>
private static readonly IReadOnlyDictionary<TournamentStatus, TournamentStatus[]> AllowedNextStates =
new Dictionary<TournamentStatus, TournamentStatus[]>
{
[TournamentStatus.Scheduled] = [TournamentStatus.OpenForRegistration, TournamentStatus.Canceled],
[TournamentStatus.OpenForRegistration] = [TournamentStatus.RegistrationClosed, TournamentStatus.Canceled],
[TournamentStatus.RegistrationClosed] = [TournamentStatus.Ongoing, TournamentStatus.Canceled],
// Ongoing may revert to RegistrationClosed to fix a division mis-assignment, tearing down the generated fixture so restarting rebuilds it correctly.
[TournamentStatus.Ongoing] = [TournamentStatus.Finished, TournamentStatus.RegistrationClosed, TournamentStatus.Canceled],
[TournamentStatus.Finished] = [],
[TournamentStatus.Canceled] = [],
};
/// <summary>
/// Whether a status is terminal, meaning it has no outgoing transition.
/// </summary>
public static bool IsTerminal(TournamentStatus status) =>
status is TournamentStatus.Finished or TournamentStatus.Canceled;
/// <summary>
/// Whether transitioning from one given status to another is allowed, with a same-status no-op always considered valid.
/// </summary>
public static bool IsValidTransition(TournamentStatus from, TournamentStatus to)
{
if (from == to)
{
return true;
}
return AllowedNextStates.TryGetValue(from, out TournamentStatus[]? nextStates)
&& nextStates.Contains(to);
}
}
@@ -0,0 +1,8 @@
namespace Domain.Enums;
public enum UserRoleType
{
ADMIN = 0,
OWNER = 1,
GUEST = 2
}
@@ -0,0 +1,9 @@
namespace Domain.Enums;
public static class UserRoleTypeExtensions
{
public static string ToRoleName(this UserRoleType role)
{
return role.ToString();
}
}