Sube carpetas API, API.Tests, Application y Domain del Backend
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
namespace Application.Utils.Constants.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Application-specific JWT claim types that are not part of the standard ClaimTypes set.
|
||||
/// </summary>
|
||||
public static class CustomClaimTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Boolean claim, stored as the string true or false, indicating the user must change their password before most endpoints.
|
||||
/// </summary>
|
||||
public const string MustChangePassword = "must_change_password";
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Application.Utils.Constants.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Durations and sizes applied when issuing JWT access/refresh tokens.
|
||||
/// </summary>
|
||||
public static class TokenLifetime
|
||||
{
|
||||
public const int AccessTokenExpiryHours = 24;
|
||||
|
||||
/// <summary>
|
||||
/// Size, in bytes, of the cryptographically random refresh token.
|
||||
/// </summary>
|
||||
public const int RefreshTokenByteLength = 64;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace Application.Utils.Constants.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Centralized configuration key paths read directly by Application and Infrastructure code.
|
||||
/// </summary>
|
||||
public static class ConfigurationKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the connection string entry under ConnectionStrings, used for both the application database and the ASP.NET Core Identity database.
|
||||
/// </summary>
|
||||
public const string DbConnection = "DbConnection";
|
||||
|
||||
public static class Jwt
|
||||
{
|
||||
public const string Key = "JWT:Key";
|
||||
public const string Issuer = "JWT:Issuer";
|
||||
public const string Audience = "JWT:Audience";
|
||||
}
|
||||
|
||||
public static class Frontend
|
||||
{
|
||||
public const string PasswordResetUrl = "Frontend:PasswordResetUrl";
|
||||
|
||||
/// <summary>
|
||||
/// Base URL of the account-activation page the invited user lands on from the invitation email.
|
||||
/// </summary>
|
||||
public const string ActivationUrl = "Frontend:ActivationUrl";
|
||||
}
|
||||
|
||||
public static class AdminUser
|
||||
{
|
||||
public const string Email = "AdminUser:Email";
|
||||
public const string Password = "AdminUser:Password";
|
||||
}
|
||||
|
||||
public static class Backup
|
||||
{
|
||||
public const string PgDumpPath = "Backup:PgDumpPath";
|
||||
public const string PsqlPath = "Backup:PsqlPath";
|
||||
}
|
||||
|
||||
public static class Supabase
|
||||
{
|
||||
public const string Section = "SupaBase";
|
||||
public const string ProjectUrl = "ProjectUrl";
|
||||
public const string ServiceRole = "ServiceRole";
|
||||
public const string BucketName = "BucketName";
|
||||
|
||||
/// <summary>
|
||||
/// Name of the private Supabase bucket medical-record PDFs are stored in, separate from BucketName.
|
||||
/// </summary>
|
||||
public const string MedicalRecordsBucketName = "MedicalRecordsBucketName";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
namespace Application.Utils.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// Centralized user-facing and exception message text, so a message never needs to be typed twice and a wording change happens in one place.
|
||||
/// </summary>
|
||||
public static class ErrorMessages
|
||||
{
|
||||
public static class Media
|
||||
{
|
||||
public const string InvalidImageFile = "El archivo de la foto debe ser una imagen JPEG/PNG válida.";
|
||||
}
|
||||
|
||||
public static class Auth
|
||||
{
|
||||
public const string EmailAlreadyExists = "Ya existe un usuario con ese email.";
|
||||
public const string InvalidCredentials = "Credenciales inválidas.";
|
||||
public const string AccountDeactivated = "Esta cuenta está desactivada.";
|
||||
public const string NoAccountForEmail = "No se encontró ninguna cuenta para ese email.";
|
||||
public const string InvalidMagicLink = "El magic-link no es válido.";
|
||||
public const string MagicLinkAlreadyUsed = "El magic-link no es válido o ya fue usado.";
|
||||
public const string InvalidPasswordResetRequest = "Solicitud de restablecimiento de contraseña inválida.";
|
||||
public const string InvalidRefreshToken = "El refresh token no es válido.";
|
||||
public const string RefreshTokenExpired = "El refresh token venció. Iniciá sesión de nuevo.";
|
||||
public const string RoleClaimMissing = "Falta el claim de rol en el token.";
|
||||
public const string IdClaimMissing = "Falta el claim de id en el token.";
|
||||
public const string AccessDenied = "Acceso denegado.";
|
||||
|
||||
public static string UserCreationFailed(string errors)
|
||||
{
|
||||
return $"No se pudo crear el usuario: {errors}";
|
||||
}
|
||||
|
||||
public static string RoleNotAllowedToCreate(string callerRole, string targetRole)
|
||||
{
|
||||
return $"El rol '{callerRole}' no puede crear usuarios con rol '{targetRole}'.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class User
|
||||
{
|
||||
public const string InsufficientPermissionsToListUsers = "No tenés permisos suficientes para listar usuarios.";
|
||||
public const string CurrentPasswordRequired = "Debés indicar la contraseña actual para cambiar tu propia contraseña.";
|
||||
public const string CannotChangeOwnActiveState = "No podés cambiar el estado activo de tu propia cuenta.";
|
||||
public const string InsufficientPermissionsToDelete = "No tenés permisos suficientes para eliminar este usuario.";
|
||||
public const string PasswordResetRestricted = "Solo Admins y Owners (para sus subordinados) pueden blanquear contraseñas.";
|
||||
public const string CannotChangeOwnRole = "No podés cambiar tu propio rol.";
|
||||
public const string InsufficientPermissionsToChangeRole = "No tenés permisos suficientes para cambiar el rol de este usuario.";
|
||||
|
||||
public static string NotFound(string userId)
|
||||
{
|
||||
return $"No se encontró el usuario '{userId}'.";
|
||||
}
|
||||
|
||||
public static string InvalidRole(object role)
|
||||
{
|
||||
return $"'{role}' no es un rol válido.";
|
||||
}
|
||||
|
||||
public static string RoleNotAllowedToAssign(string callerRole, string targetRole)
|
||||
{
|
||||
return $"El rol '{callerRole}' no puede asignar el rol '{targetRole}'.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Tournament
|
||||
{
|
||||
/// <summary>
|
||||
/// Blocks reverting an Ongoing tournament that already has results.
|
||||
/// </summary>
|
||||
public const string CannotRevertWithPlayedMatches =
|
||||
"No se puede revertir a borrador: el torneo ya tiene partidos jugados. " +
|
||||
"Sólo se puede revertir un torneo en curso sin resultados cargados.";
|
||||
|
||||
public static string NotFound(System.Guid tournamentId)
|
||||
{
|
||||
return $"No existe un torneo con id: {tournamentId}.";
|
||||
}
|
||||
|
||||
public static string InvalidStatusTransition(
|
||||
Domain.Enums.TournamentStatus from, Domain.Enums.TournamentStatus to)
|
||||
{
|
||||
return $"No se puede cambiar el estado del torneo de '{from}' a '{to}'. " +
|
||||
"El flujo permitido es Scheduled -> OpenForRegistration -> RegistrationClosed -> " +
|
||||
"Ongoing -> Finished; un torneo puede pasar a Canceled desde cualquier estado no terminal, " +
|
||||
"y Finished/Canceled son terminales.";
|
||||
}
|
||||
|
||||
public static string StructuralEditNotAllowed(Domain.Enums.TournamentStatus status)
|
||||
{
|
||||
return "Los cambios estructurales (divisiones e inscripción de equipos) solo se permiten " +
|
||||
$"mientras el torneo está OpenForRegistration; este torneo está '{status}'.";
|
||||
}
|
||||
|
||||
public static string CategoryMismatch(
|
||||
Domain.Enums.TournamentCategory divisionCategory,
|
||||
Domain.Enums.TournamentCategory tournamentCategory)
|
||||
{
|
||||
return $"Una división '{divisionCategory}' no puede pertenecer a un torneo '{tournamentCategory}' " +
|
||||
"(HU-48): la competencia femenina es un torneo aparte y no puede mezclarse con la masculina. " +
|
||||
"Creá la división dentro de un torneo de la misma categoría.";
|
||||
}
|
||||
|
||||
public static string NotCompletable(string issueSummaries)
|
||||
{
|
||||
return "No se puede iniciar el torneo porque no está completo en su estado actual (HU-109): " +
|
||||
$"{issueSummaries}. Resolvé todos los problemas antes de iniciar.";
|
||||
}
|
||||
|
||||
public static string UnenrollNotAllowed(Domain.Enums.TournamentStatus status)
|
||||
{
|
||||
return "Los equipos solo se pueden dar de baja mientras el torneo está OpenForRegistration o " +
|
||||
$"RegistrationClosed; este torneo está '{status}'.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message blocking deletion of a tournament that already started or has played matches, to avoid erasing competitive history.
|
||||
/// </summary>
|
||||
public const string HasHistoryCannotDelete =
|
||||
"No se puede eliminar el torneo: tiene partidos jugados o ya arrancó.";
|
||||
}
|
||||
|
||||
public static class Division
|
||||
{
|
||||
/// <summary>
|
||||
/// User-facing Spanish message blocking deletion of a division that already has played matches or point deductions, to avoid cascading away that competitive history.
|
||||
/// </summary>
|
||||
public const string HasHistoryCannotDelete =
|
||||
"No se puede eliminar la división: tiene partidos jugados o deducciones de puntos registradas.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message blocking deletion of a division whose tournament fixture is already generated.
|
||||
/// </summary>
|
||||
public const string StructureLockedTournamentStarted =
|
||||
"No se puede eliminar la división: el torneo ya arrancó o fue cancelado.";
|
||||
|
||||
public static string ConflictingRosterEnrollment(string teamIds)
|
||||
{
|
||||
return $"No se puede inscribir el/los equipo(s) {teamIds} en esta división: ya está(n) inscripto(s) en otra división del mismo torneo.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Team
|
||||
{
|
||||
public static string NotFound(System.Guid teamId)
|
||||
{
|
||||
return $"No existe un equipo con id: {teamId}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message blocking deletion of a team that carries competitive history including match participations, sanctions, deductions or registrations.
|
||||
/// </summary>
|
||||
public const string HasHistoryCannotDelete =
|
||||
"No se puede eliminar el equipo: tiene historial de partidos, sanciones, deducciones o inscripciones a torneos.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message blocking edits to Team.Name or Team.ThreeLetterCode while the team plays in an Ongoing tournament.
|
||||
/// </summary>
|
||||
public const string IdentityFrozenWhileOngoing =
|
||||
"No se puede cambiar el nombre ni la sigla del equipo mientras participa en un torneo en curso. " +
|
||||
"Podés editar los colores y el escudo.";
|
||||
|
||||
public static string NotInTournament(System.Guid teamId)
|
||||
{
|
||||
return $"El equipo '{teamId}' no está inscripto en ningún torneo actualmente, así que no se pueden registrar jugadores en él.";
|
||||
}
|
||||
|
||||
public static string AlreadyEnrolled(System.Guid teamId, System.Guid tournamentId)
|
||||
{
|
||||
return $"El equipo '{teamId}' ya está inscripto en el torneo '{tournamentId}'. Un equipo solo puede inscribirse una vez por torneo.";
|
||||
}
|
||||
|
||||
public static string NotEnrolled(System.Guid teamId, System.Guid tournamentId)
|
||||
{
|
||||
return $"El equipo '{teamId}' no está inscripto en el torneo '{tournamentId}'.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Club
|
||||
{
|
||||
public static string NotFound(System.Guid clubId)
|
||||
{
|
||||
return $"No existe un club con id: {clubId}.";
|
||||
}
|
||||
|
||||
public const string CannotLinkToItself =
|
||||
"Un club no puede vincularse a sí mismo como club matriz.";
|
||||
|
||||
public const string ParentAlreadyHasParent =
|
||||
"El club matriz elegido ya es una escuadra de otro club; solo se admite un nivel de vinculación.";
|
||||
|
||||
public const string CannotBecomeChildWithExistingSquads =
|
||||
"Este club ya tiene escuadras vinculadas, así que no puede pasar a ser una escuadra de otro club.";
|
||||
|
||||
public const string NameRequired =
|
||||
"El nombre del club es obligatorio.";
|
||||
|
||||
public const string HasTeamsOrSquadsCannotDelete =
|
||||
"No se puede eliminar un club con equipos o escuadras vinculadas.";
|
||||
}
|
||||
|
||||
public static class Roster
|
||||
{
|
||||
public static string PlayerAlreadyInAnotherTeam(System.Guid playerId, System.Guid tournamentId)
|
||||
{
|
||||
return $"El jugador '{playerId}' ya está registrado en otro equipo del torneo '{tournamentId}'. Un jugador no puede estar en dos equipos del mismo torneo.";
|
||||
}
|
||||
|
||||
public static string RosterFull(System.Guid teamId, int maxPlayers)
|
||||
{
|
||||
return $"El equipo '{teamId}' ya tiene el máximo de {maxPlayers} jugadores para este torneo.";
|
||||
}
|
||||
|
||||
public static string DuplicateJerseyNumber(int jerseyNumber, System.Guid teamId, System.Guid tournamentId)
|
||||
{
|
||||
return $"El dorsal {jerseyNumber} ya lo usa otro jugador del equipo '{teamId}' en el torneo '{tournamentId}'.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Player
|
||||
{
|
||||
/// <summary>
|
||||
/// User-facing Spanish message blocking deletion of a player who has match statistics, scorer records or sanctions, to avoid orphaning historical records.
|
||||
/// </summary>
|
||||
public const string HasHistoryCannotDelete =
|
||||
"No se puede eliminar: el jugador tiene estadísticas o sanciones registradas.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message blocking a mid-season team move for a player who already has match statistics, scorer records or sanctions that season.
|
||||
/// </summary>
|
||||
public const string CannotMoveTeamWithHistory =
|
||||
"No se puede cambiar de equipo a un jugador que ya tiene estadísticas o sanciones registradas en esta temporada.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message returned when a player is created or updated with a DocumentNumber another player already has.
|
||||
/// </summary>
|
||||
public static string DuplicateDocumentNumber(string documentNumber)
|
||||
{
|
||||
return $"Ya existe un jugador registrado con el documento {documentNumber}.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Venue
|
||||
{
|
||||
/// <summary>
|
||||
/// User-facing Spanish message blocking deletion of a venue that is referenced by one or more matches.
|
||||
/// </summary>
|
||||
public const string ReferencedByMatches =
|
||||
"No se puede eliminar: la cancha tiene partidos asociados.";
|
||||
}
|
||||
|
||||
public static class Stage
|
||||
{
|
||||
public const string NotFoundGeneric = "No se encontró la fase.";
|
||||
public const string DivisionNotFound = "No se encontró la división.";
|
||||
public const string DivisionAlreadyHasStages = "No se puede procesar la solicitud porque la división ya tiene alguna fase.";
|
||||
public const string GenerateMatchesBeforeSeeding = "Generá los partidos de esta fase antes de sembrarla.";
|
||||
public const string AlreadySeeded = "Esta fase ya fue sembrada.";
|
||||
public const string InvalidStageType = "Tipo de fase inválido.";
|
||||
public const string SeedMissingStandings = "No se puede sembrar: todavía no todos los equipos asignados a esta fase tienen una posición de la fase de grupos finalizada.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message blocking adding or removing a stage once the division's tournament has already started.
|
||||
/// </summary>
|
||||
public const string StructureLockedTournamentStarted =
|
||||
"No se pueden agregar o quitar fases: el torneo ya arrancó o fue cancelado.";
|
||||
|
||||
public static string NotFoundById(System.Guid id)
|
||||
{
|
||||
return $"No se encontró la fase con id {id}.";
|
||||
}
|
||||
|
||||
public static string NotFoundById(string idOrSlug)
|
||||
{
|
||||
return $"No se encontró la fase con id o slug {idOrSlug}.";
|
||||
}
|
||||
|
||||
public static string AlreadyExistsInDivision(string stageName)
|
||||
{
|
||||
return $"Ya existe una fase con el nombre '{stageName}' en la división actual.";
|
||||
}
|
||||
|
||||
public static string MaxTeamsReached(int maxTeams)
|
||||
{
|
||||
return $"Esta fase ya tiene el máximo de {maxTeams} equipos.";
|
||||
}
|
||||
|
||||
public static string NotEnoughSlots(int requested, int available)
|
||||
{
|
||||
return $"No se pueden agregar {requested} equipos. Solo hay {available} lugares disponibles.";
|
||||
}
|
||||
|
||||
public static string InvalidTournamentSize(int registeredTeams, string validSizes)
|
||||
{
|
||||
return $"Cantidad inválida de equipos inscriptos: {registeredTeams}. Los tamaños válidos son {validSizes} equipos.";
|
||||
}
|
||||
|
||||
public static string TeamsNotDivisibleForGroups(int registeredTeams, int groupSize)
|
||||
{
|
||||
return $"La cantidad de equipos inscriptos ({registeredTeams}) debe ser divisible por {groupSize} para generar las fases de grupos.";
|
||||
}
|
||||
|
||||
public static string ConflictingTeamAssignment(string teamIds)
|
||||
{
|
||||
return $"No se puede asignar el/los equipo(s) {teamIds} a esta división: ya está(n) asignado(s) a otra división del mismo torneo.";
|
||||
}
|
||||
|
||||
public static string TeamNotEnrolledInDivision(string teamIds)
|
||||
{
|
||||
return $"No se puede asignar el/los equipo(s) {teamIds} a esta fase: no está(n) inscripto(s) en el roster de la división.";
|
||||
}
|
||||
|
||||
public static string SeedTeamCountOutOfRange(int assignedCount, int slotCapacity)
|
||||
{
|
||||
return $"No se puede sembrar: hay {assignedCount} equipo(s) asignado(s) a esta fase, se esperaba entre 2 y {slotCapacity}. " +
|
||||
"Una cantidad de equipos menor al cuadro completo está bien (los mejores seeds pasan con bye), pero no puede superar los lugares generados.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message blocking a playoffs-only draw preview or commit on a division that still has a group phase.
|
||||
/// </summary>
|
||||
public const string DrawRequiresGrouplessDivision =
|
||||
"El sorteo de llave es solo para divisiones sin fase de grupos; esta división ya tiene una fase de grupos.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message rejecting a draw commit whose token is missing, tampered, expired, or does not match the stage or roster.
|
||||
/// </summary>
|
||||
public const string InvalidDrawToken =
|
||||
"El token del sorteo no es válido o no corresponde a esta fase. Volvé a previsualizar el sorteo.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message rejecting a manual seeding order that is not exactly a permutation of the division's enrolled roster.
|
||||
/// </summary>
|
||||
public const string ManualOrderNotRosterPermutation =
|
||||
"El orden manual debe incluir exactamente a los equipos inscriptos en el roster de la división, sin repetir ni omitir ninguno.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message blocking a bracket draw or re-draw once a real match of that bracket has already been played.
|
||||
/// </summary>
|
||||
public const string BracketAlreadyPlayed =
|
||||
"No se puede sortear ni resortear esta llave: ya hay al menos un partido jugado en este cuadro.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message rejecting a sub-group count below 1.
|
||||
/// </summary>
|
||||
public const string SubGroupCountMustBePositive =
|
||||
"La cantidad de sub-grupos debe ser mayor o igual a 1.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message rejecting a manual reassignment for a team not currently placed in the source sub-group.
|
||||
/// </summary>
|
||||
public const string TeamNotPlacedInSubGroup =
|
||||
"El equipo no está ubicado en el sub-grupo de origen indicado.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message rejecting a manual reassignment across two sub-groups of different divisions.
|
||||
/// </summary>
|
||||
public const string ReassignmentAcrossDivisionsNotAllowed =
|
||||
"No se puede reasignar un equipo entre sub-grupos de distintas divisiones.";
|
||||
|
||||
public static string SubGroupTooFewTeams(int teamCount, int subGroupCount)
|
||||
{
|
||||
return $"No se pueden crear {subGroupCount} sub-grupos con {teamCount} equipo(s) inscripto(s): cada sub-grupo necesita como mínimo 4 equipos. Elegí una cantidad de sub-grupos menor.";
|
||||
}
|
||||
|
||||
public static string SubGroupReassignmentBelowMinimum(int remainingTeams)
|
||||
{
|
||||
return $"No se puede mover el equipo: el sub-grupo de origen quedaría con {remainingTeams} equipo(s), por debajo del mínimo de 4.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message rejecting a division split into 2+ groups that has no cup to determine an overall champion between them.
|
||||
/// </summary>
|
||||
public const string SubGroupsRequireCupToDetermineChampion =
|
||||
"Una división con 2 o más grupos necesita al menos una copa: sin ella no hay forma de determinar un campeón entre los grupos.";
|
||||
}
|
||||
|
||||
public static class Match
|
||||
{
|
||||
public const string CannotUpdateStartedOrFinished = "No se puede editar un partido que ya arrancó o finalizó.";
|
||||
public const string TeamsNotAssignedToStage = "No se puede actualizar la fecha del partido porque uno o ambos equipos no están asignados a la fase.";
|
||||
public const string VenueScheduleConflict =
|
||||
"Esa cancha ya tiene otro partido a menos de 2 horas de esa hora. " +
|
||||
"Elegí otro horario (mínimo 2 horas de diferencia) u otra cancha.";
|
||||
public const string StageAlreadyHasMatches = "No se puede procesar la solicitud porque la fase actual ya tiene partidos.";
|
||||
public const string NoGroupStagesForDivision = "No se encontraron fases de grupos para la división.";
|
||||
public const string NoTeamsRegistered = "No hay equipos inscriptos en el torneo.";
|
||||
public const string NotEnoughTeamsPerGroup = "Se necesitan al menos 2 equipos por grupo para generar los partidos.";
|
||||
public const string InvalidKnockoutStageType = "Tipo de fase eliminatoria inválido.";
|
||||
public const string MatchCountMustBePositive = "La cantidad de partidos debe ser mayor a cero.";
|
||||
public const string EndDateBeforeStartDate = "La fecha de fin debe ser posterior a la fecha de inicio.";
|
||||
public const string StageTypeNotSupportedForAutomatedCreation = "Este tipo de fase no admite la generación automática de partidos.";
|
||||
|
||||
// Basketball has no draws — a played match must have a winner.
|
||||
public const string GroupStageTieNotAllowed =
|
||||
"Un partido de fase de grupos no puede terminar empatado; el básquet no tiene empates. Cargá un resultado con un ganador.";
|
||||
public const string PlayoffTieNotAllowed =
|
||||
"Un partido de playoff no puede terminar empatado; debe resolverse en un tiempo suplementario. Cargá el resultado final con un ganador.";
|
||||
|
||||
public const string WalkOverTeamNotInMatch =
|
||||
"El equipo presente tiene que ser el local o el visitante de este partido.";
|
||||
|
||||
public static string TeamsNotDistributableAcrossGroups(int registeredTeams, int totalGroups)
|
||||
{
|
||||
return $"Los equipos inscriptos ({registeredTeams}) no se pueden distribuir de forma pareja entre {totalGroups} grupos.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class MatchSheet
|
||||
{
|
||||
// The sum of a team's players' points must equal the team's final score.
|
||||
public static string ScoreMismatch(int teamScore, int playersSum)
|
||||
{
|
||||
int difference = teamScore - playersSum;
|
||||
return $"Los puntos de los jugadores no coinciden con el resultado del equipo: el equipo anotó {teamScore} " +
|
||||
$"pero la suma de los jugadores cargados da {playersSum} (diferencia de {difference}). Corregí la planilla antes de guardar.";
|
||||
}
|
||||
|
||||
public static string MatchNotFinished(System.Guid matchId)
|
||||
{
|
||||
return $"No se puede cargar la planilla del partido {matchId} porque todavía no tiene un resultado final cargado.";
|
||||
}
|
||||
|
||||
public static string MatchMissingTeams(System.Guid matchId)
|
||||
{
|
||||
return $"El partido {matchId} todavía no tiene los dos equipos asignados, así que no se le puede cargar un resultado.";
|
||||
}
|
||||
|
||||
public static string TeamNotInMatch(System.Guid teamId)
|
||||
{
|
||||
return $"El equipo {teamId} no jugó este partido, así que no se pueden cargar sus puntos acá.";
|
||||
}
|
||||
|
||||
public static string PlayerNotOnRosterReason(string playerLabel)
|
||||
{
|
||||
return $"{playerLabel} (no está en el plantel del equipo para esta temporada)";
|
||||
}
|
||||
|
||||
public static string PlayerNotEligibleReason(string playerLabel)
|
||||
{
|
||||
return $"{playerLabel} (no está habilitado: le falta la inscripción aprobada o tiene una sanción activa)";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One combined error naming every ineligible or off-roster player across both teams' sheets, grouped by team.
|
||||
/// </summary>
|
||||
public static string PlayersNotEligible(
|
||||
System.Collections.Generic.IEnumerable<(string TeamName, System.Collections.Generic.List<string> Reasons)> issuesByTeam)
|
||||
{
|
||||
System.Collections.Generic.List<string> groups = [];
|
||||
foreach ((string teamName, System.Collections.Generic.List<string> reasons) in issuesByTeam)
|
||||
{
|
||||
groups.Add($"{teamName}: {string.Join(", ", reasons)}.");
|
||||
}
|
||||
|
||||
return $"No se puede cargar el resultado porque hay jugadores no elegibles. {string.Join(" ", groups)}";
|
||||
}
|
||||
|
||||
public static string TeamRequiresWalkOver(string teamName, int habilitadoCount)
|
||||
{
|
||||
return $"{teamName} tiene {habilitadoCount} jugador(es) habilitado(s) (mínimo 4), así que este partido no se puede " +
|
||||
"cargar con un resultado normal. Cargalo como walkover.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class MedicalRecord
|
||||
{
|
||||
public const string InvalidPdfFile = "El archivo de la ficha médica debe ser un PDF válido.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message returned when a new ficha upload is attempted on a registration whose medical record is already Approved.
|
||||
/// </summary>
|
||||
public const string AlreadyApproved =
|
||||
"La ficha médica ya está aprobada; no se puede subir una nueva. Solo puede consultarse o descargarse.";
|
||||
|
||||
/// <summary>
|
||||
/// User-facing Spanish message returned when an approve is attempted against a registration with no real stored file.
|
||||
/// </summary>
|
||||
public const string NoStoredFile =
|
||||
"No se puede aprobar la ficha médica: no hay un archivo cargado. Subí la ficha antes de aprobarla.";
|
||||
|
||||
public static string RegistrationNotFound(
|
||||
System.Guid playerId, System.Guid teamId, System.Guid tournamentId)
|
||||
{
|
||||
return $"El jugador {playerId} no tiene una inscripción al equipo {teamId} para el torneo " +
|
||||
$"{tournamentId}, así que no se puede adjuntar ni revisar una ficha médica.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class MatchSeries
|
||||
{
|
||||
public const string RequiresTwoDifferentTeams = "Una serie requiere dos equipos distintos.";
|
||||
public const string AlreadyExistsForStage = "Ya existe una serie entre estos dos equipos para esta fase.";
|
||||
public const string NotFound = "No se encontró la serie.";
|
||||
public const string AlreadyDecided = "No se puede agregar un partido a una serie que ya se definió.";
|
||||
|
||||
public static string NotFoundById(System.Guid id)
|
||||
{
|
||||
return $"No se encontró la serie con id {id}.";
|
||||
}
|
||||
|
||||
public static string MaxGamesReached(int bestOf)
|
||||
{
|
||||
return $"Esta serie ya tiene el máximo de {bestOf} partidos.";
|
||||
}
|
||||
|
||||
public static string TeamNotAssignedToStage(System.Guid teamId)
|
||||
{
|
||||
return $"El equipo {teamId} no está asignado a esta fase.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayerSanction
|
||||
{
|
||||
public const string AppealAlreadyPending = "Esta sanción ya tiene una apelación pendiente.";
|
||||
public const string NoPendingAppealToResolve = "No hay ninguna apelación pendiente para resolver en esta sanción.";
|
||||
}
|
||||
|
||||
public static class Playoff
|
||||
{
|
||||
public const string NotEnoughRankedTeams = "Para sembrar se necesitan al menos dos equipos rankeados.";
|
||||
public const string EmptyDestination = "Un rango de posiciones de playoff debe tener un destino no vacío.";
|
||||
public const string NoMappingsConfigured = "Esta división no tiene ningún mapeo de rango de posiciones a playoff configurado.";
|
||||
|
||||
public static string InvalidRange(int from, int to)
|
||||
{
|
||||
return $"Rango de posiciones de playoff inválido {from}-{to}: las posiciones empiezan en 1 y 'desde' debe ser menor o igual a 'hasta'.";
|
||||
}
|
||||
|
||||
public static string OverlappingRanges(int firstFrom, int firstTo, int secondFrom, int secondTo)
|
||||
{
|
||||
return $"Los rangos de posiciones de playoff {firstFrom}-{firstTo} y {secondFrom}-{secondTo} se superponen; cada posición debe mapear a un solo destino.";
|
||||
}
|
||||
|
||||
public static string CupStageNotFound(string destination)
|
||||
{
|
||||
return $"No se encontró una fase eliminatoria de primera ronda sin sembrar para el destino de playoff '{destination}'.";
|
||||
}
|
||||
|
||||
public static string InvalidQualifiersPerGroup(int qualifiersPerGroup)
|
||||
{
|
||||
return $"QualifiersPerGroup debe ser al menos 1, pero era {qualifiersPerGroup}.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Backup
|
||||
{
|
||||
public const string RetentionCountNegative = "Retention count cannot be negative.";
|
||||
public const string OperationInProgress = "A backup or restore operation is already in progress.";
|
||||
}
|
||||
|
||||
public static class Configuration
|
||||
{
|
||||
public const string ConnectionStringMissing = "The connection string should be initialized already.";
|
||||
public const string JwtMissing = "The JWT is missing or empty in configuration.";
|
||||
|
||||
public static string SmtpKeyMissing(string key)
|
||||
{
|
||||
return $"{key} is missing from configuration.";
|
||||
}
|
||||
|
||||
public static string KeyNotConfigured(string key)
|
||||
{
|
||||
return $"{key} is not configured.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Storage
|
||||
{
|
||||
public static string UploadFailed(string reason)
|
||||
{
|
||||
return $"Error uploading file: {reason}";
|
||||
}
|
||||
|
||||
public static string DeleteFailed(string reason)
|
||||
{
|
||||
return $"Error deleting file: {reason}";
|
||||
}
|
||||
|
||||
public static string ListFailed(string reason)
|
||||
{
|
||||
return $"Error listing files: {reason}";
|
||||
}
|
||||
|
||||
public static string RemoveFailed(string reason)
|
||||
{
|
||||
return $"Error removing file: {reason}";
|
||||
}
|
||||
|
||||
public static string DownloadFailed(string reason)
|
||||
{
|
||||
return $"Error downloading file: {reason}";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Query
|
||||
{
|
||||
public const string ContainsMethodNotFound = "The 'Contains' method could not be found on the string class.";
|
||||
public const string ToLowerMethodNotFound = "The 'ToLower' method could not be found on the string class.";
|
||||
}
|
||||
|
||||
public static class Serialization
|
||||
{
|
||||
public static string InvalidDate(string? rawValue)
|
||||
{
|
||||
return $"Invalid date: '{rawValue}'.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Application.Utils.Constants.Pagination;
|
||||
|
||||
/// <summary>
|
||||
/// Default and boundary values applied to paginated list requests when the caller does not specify a page size, or supplies an out-of-range one.
|
||||
/// </summary>
|
||||
public static class PaginationDefaults
|
||||
{
|
||||
public const int DefaultPageSize = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed page size; requests above this are clamped down to it.
|
||||
/// </summary>
|
||||
public const int MaxPageSize = 100;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Application.Utils.Constants.Scorer;
|
||||
|
||||
/// <summary>
|
||||
/// Points awarded per match outcome when computing the team scoreboard.
|
||||
/// </summary>
|
||||
public static class ScoreConstants
|
||||
{
|
||||
public const int PointsForWin = 2;
|
||||
public const int PointsForLoss = 1;
|
||||
public const int PointsForDraw = 1;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Application.Utils.Constants.Stage;
|
||||
|
||||
/// <summary>
|
||||
/// Provides constant values representing the number of matches generated for each knockout-stage type during automated match creation.
|
||||
/// </summary>
|
||||
public static class KnockoutMatchCount
|
||||
{
|
||||
public const int QuarterFinal = 4;
|
||||
|
||||
public const int SemiFinal = 2;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Application.Utils.Constants.Stage;
|
||||
|
||||
public static class MaxTeams
|
||||
{
|
||||
/// <summary>
|
||||
/// Standard number of teams per group used to partition a division's registered teams into same-sized Group stages.
|
||||
/// </summary>
|
||||
public const int Group = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Upper bound on how many teams a single manually-built Group stage, one zone's whole round-robin phase, may hold.
|
||||
/// </summary>
|
||||
public const int GroupStageCap = 32;
|
||||
|
||||
public const int QuarterFinal = 8;
|
||||
|
||||
public const int SemiFinal = 4;
|
||||
|
||||
public const int ThirdPlace = 2;
|
||||
|
||||
public const int Final = 2;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Application.Utils.Constants.Stage;
|
||||
|
||||
/// <summary>
|
||||
/// Bounds for how many times each pair of teams plays within a single group stage, RoundRobinLegs.
|
||||
/// </summary>
|
||||
public static class RoundRobinFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Single round-robin: every pair plays once.
|
||||
/// </summary>
|
||||
public const int MinLegs = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Upper bound to keep fixture size sane, a triple round-robin.
|
||||
/// </summary>
|
||||
public const int MaxLegs = 3;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Application.Utils.Constants.Stage;
|
||||
|
||||
public static class StageTemplate
|
||||
{
|
||||
public static readonly Template Group = new("Fase de Grupos", "Fase de grupos todos contra todos.");
|
||||
|
||||
public static readonly Template RoundOf16 = new("Octavos de Final", "Etapa eliminatoria con 16 equipos.");
|
||||
|
||||
public static readonly Template QuarterFinal = new("Cuartos de Final", "Etapa eliminatoria con 8 equipos.");
|
||||
|
||||
public static readonly Template SemiFinal = new("Semifinales", "Etapa eliminatoria con 4 equipos.");
|
||||
|
||||
public static readonly Template Final = new("Final", "Partido final del campeonato.");
|
||||
|
||||
public static readonly Template ThirdPlace = new("Tercer Puesto", "Partido para determinar el tercer lugar.");
|
||||
|
||||
public static readonly int DurationDays = 7;
|
||||
|
||||
/// <summary>
|
||||
/// Standard rest gap, in days, inserted between two consecutive automated stages, from the group stage to the quarter-final.
|
||||
/// </summary>
|
||||
public static readonly int StandardGapDays = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Shorter rest gap, in days, inserted between the semi-final and the third place match.
|
||||
/// </summary>
|
||||
public static readonly int ThirdPlaceGapDays = 1;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Application.Utils.Constants.Stage;
|
||||
|
||||
public record Template(string Name, string Description);
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
||||
namespace Application.Utils.Constants.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a string is a plausible Argentine phone number.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
|
||||
public sealed class ArgentinePhoneNumberAttribute : ValidationAttribute
|
||||
{
|
||||
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
|
||||
{
|
||||
if (value is not string phone || string.IsNullOrWhiteSpace(phone))
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
string trimmed = phone.Trim();
|
||||
bool hasOnlyAllowedChars = trimmed.All(c => char.IsDigit(c) || c is '+' or ' ' or '(' or ')' or '-');
|
||||
int digitCount = trimmed.Count(char.IsDigit);
|
||||
|
||||
return hasOnlyAllowedChars && digitCount == 10
|
||||
? ValidationResult.Success
|
||||
: new ValidationResult(
|
||||
ErrorMessage ?? ValidationPatterns.PhoneNumberError,
|
||||
[validationContext.MemberName ?? string.Empty]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Application.Utils.Constants.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Field length limits shared by every division-name-accepting request DTO and entity configuration.
|
||||
/// </summary>
|
||||
public static class DivisionFieldLengths
|
||||
{
|
||||
public const int NameMaxLength = 100;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.Utils.Constants.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a DateTime birth date is at least minimumYears years in the past, meaning the person is at least that many years old today.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
|
||||
public sealed class MinimumAgeAttribute(int minimumYears) : ValidationAttribute
|
||||
{
|
||||
private readonly int _minimumYears = minimumYears;
|
||||
|
||||
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
|
||||
{
|
||||
if (value is not DateTime birthDate)
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
DateTime cutoff = DateTime.UtcNow.Date.AddYears(-_minimumYears);
|
||||
|
||||
return birthDate.Date <= cutoff
|
||||
? ValidationResult.Success
|
||||
: new ValidationResult(
|
||||
ErrorMessage ?? $"El jugador debe tener al menos {_minimumYears} años.",
|
||||
[validationContext.MemberName ?? string.Empty]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Application.Utils.Constants.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Field length limits shared by CreatePlayerRequest and UpdatePlayerRequest, so the two stay in sync.
|
||||
/// </summary>
|
||||
public static class PlayerFieldLengths
|
||||
{
|
||||
public const int NameMaxLength = 70;
|
||||
public const int DocumentNumberMaxLength = 15;
|
||||
public const int PhoneNumberMaxLength = 15;
|
||||
public const int SocialSecurityMaxLength = 100;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Application.Utils.Constants.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Field length limits shared across the player sanction request DTOs, so they stay in sync.
|
||||
/// </summary>
|
||||
public static class SanctionFieldLengths
|
||||
{
|
||||
/// <summary>
|
||||
/// Max length of the short sanction description on Create and Update requests.
|
||||
/// </summary>
|
||||
public const int DescriptionMaxLength = 255;
|
||||
|
||||
/// <summary>
|
||||
/// Min length of the free-text appeal reason / resolution notes.
|
||||
/// </summary>
|
||||
public const int LongTextMinLength = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Max length of the free-text appeal reason / resolution notes.
|
||||
/// </summary>
|
||||
public const int LongTextMaxLength = 1000;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Application.Utils.Constants.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Field length limits shared by CreateSeasonRequest and UpdateSeasonRequest, so the two stay in sync.
|
||||
/// </summary>
|
||||
public static class SeasonFieldLengths
|
||||
{
|
||||
public const int NameMaxLength = 100;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Application.Utils.Constants.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Field length limits shared across the account and user request DTOs, so they stay in sync.
|
||||
/// </summary>
|
||||
public static class UserFieldLengths
|
||||
{
|
||||
public const int UsernameMinLength = 3;
|
||||
public const int UsernameMaxLength = 50;
|
||||
public const int PasswordMinLength = 8;
|
||||
public const int PhoneMaxLength = 15;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace Application.Utils.Constants.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Shared validation regex patterns kept in one place so every request DTO and the frontend validators enforce the exact same plausible contact-field rules.
|
||||
/// </summary>
|
||||
public static class ValidationPatterns
|
||||
{
|
||||
/// <summary>
|
||||
/// Plausible phone number: only digits, spaces, plus, hyphen and parentheses, containing between 8 and 15 digits.
|
||||
/// </summary>
|
||||
public const string PhoneNumber = @"^(?=(?:\D*\d){8,15}\D*$)[+\d\s()-]+$";
|
||||
|
||||
/// <summary>
|
||||
/// Human-facing message for a phone number that fails the pattern.
|
||||
/// </summary>
|
||||
public const string PhoneNumberError = "Invalid phone number format.";
|
||||
|
||||
/// <summary>
|
||||
/// A player's DNI or document number: digits only, 6 to 15 of them.
|
||||
/// </summary>
|
||||
public const string DocumentNumber = @"^\d{6,15}$";
|
||||
|
||||
/// <summary>
|
||||
/// Human-facing message for a document number that fails the pattern.
|
||||
/// </summary>
|
||||
public const string DocumentNumberError = "El documento debe contener solo números.";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Application.Utils.Constants.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Field length limits shared by CreateVenueRequest and UpdateVenueRequest, so the two stay in sync.
|
||||
/// </summary>
|
||||
public static class VenueFieldLengths
|
||||
{
|
||||
public const int NameMaxLength = 50;
|
||||
public const int AddressMaxLength = 200;
|
||||
}
|
||||
Reference in New Issue
Block a user