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,17 @@
using Application.DTOs.AuditLogs.Response;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class AuditLogProfile : Profile
{
public AuditLogProfile()
{
_ = CreateMap<AuditLog, AuditLogResponse>()
.ForMember(dest => dest.Action, opt => opt.MapFrom(src => src.Action.ToString()))
.ForMember(dest => dest.Timestamp, opt => opt.MapFrom(src => src.DateCreated));
}
}
@@ -0,0 +1,28 @@
using Application.DTOs.BlogPosts.Request;
using Application.DTOs.BlogPosts.Response;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class BlogPostProfile : Profile
{
public BlogPostProfile()
{
_ = CreateMap<CreateBlogPostRequest, BlogPost>();
_ = CreateMap<BlogPost, BlogPostResponse>()
.ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => src.DateCreated))
.ReverseMap();
_ = CreateMap<UpdateBlogPostRequest, BlogPost>()
// Null IsPublished on the request means leave unchanged, so only overwrite the entity's flag when the caller sent a value.
.ForMember(dest => dest.IsPublished, opt =>
{
opt.PreCondition(src => src.IsPublished.HasValue);
opt.MapFrom(src => src.IsPublished!.Value);
});
}
}
@@ -0,0 +1,43 @@
using Application.DTOs.Divisions.Request;
using Application.DTOs.Divisions.Response;
using Application.DTOs.Tournament.Response;
using Application.Utils.Helper.Playoff;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class DivisionProfile : Profile
{
public DivisionProfile()
{
_ = CreateMap<Division, DivisionResponse>()
// QualificationRanges has no Division counterpart, so it is computed here from PlayoffMappings and ignored on the reverse map.
.ForMember(
dest => dest.QualificationRanges,
opt => opt.MapFrom(src => QualificationRangeBuilder.Build(src.PlayoffMappings)))
// TournamentSlug is resolved from the Tournament navigation, degrading to null when it was not included.
.ForMember(
dest => dest.TournamentSlug,
opt => opt.MapFrom(src => src.Tournament != null ? src.Tournament.Slug : null))
.ReverseMap();
_ = CreateMap<Division, MinimalDivisionResponse>()
.ReverseMap();
_ = CreateMap<PlayoffMappingRequest, DivisionPlayoffMapping>();
_ = CreateMap<DivisionPlayoffMapping, PlayoffMappingResponse>();
// Tournament cloning (HU-cloning): Stages resolves to StageStructureResponse
// once StageProfile registers that map.
_ = CreateMap<Division, DivisionStructureResponse>();
_ = CreateMap<CreateDivisionRequest, Division>();
// TournamentId is deliberately excluded from the blind mapping, since an omitted value would map to Guid via GetValueOrDefault and silently zero out the division's tournament, so reassignment must go through DivisionService.TryAssignTournamentAsync instead.
_ = CreateMap<UpdateDivisionRequest, Division>()
.ForMember(dest => dest.TournamentId, opt => opt.Ignore());
}
}
@@ -0,0 +1,138 @@
using Application.DTOs.Match.Request;
using Application.DTOs.Match.Response;
using Application.DTOs.Player.Response;
using Application.DTOs.Scorer.Response;
using AutoMapper;
using Domain.Entities.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace API.AutoMapperProfiles;
public class MatchProfile : Profile
{
public MatchProfile()
{
_ = CreateMap<CreateMatchRequest, Match>();
_ = CreateMap<Match, DetailedMatchResponse>()
.ForMember(dest => dest.MatchType, opt => opt.MapFrom(src => src.Type.ToString()))
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status.ToString()))
.ForMember(dest => dest.HomeTeam, opt => opt.MapFrom(src => src.HomeTeam))
.ForMember(dest => dest.VisitorTeam, opt => opt.MapFrom(src => src.VisitorTeam))
.ForMember(dest => dest.WinningTeamName, opt => opt.MapFrom(src => src.WinningTeam != null ? src.WinningTeam.Name : null))
.ForMember(dest => dest.WinningTeamId, opt => opt.MapFrom(src => src.WinningTeam != null ? src.WinningTeam.Id : (Guid?) null))
// Score and Scorers are set here instead of via ForPath, since ForPath would force AutoMapper to instantiate HomeTeam even when src.HomeTeam is null, producing a fake team instead of an empty bracket slot.
.AfterMap((src, dest) =>
{
// Each match's scorers are attributed to a team via the player's TeamId since Scorer itself has no TeamId, which requires the match to be loaded with Scorers.Player.
Guid? tournamentId = src.Stage?.Division?.TournamentId;
if (dest.HomeTeam is not null)
{
dest.HomeTeam.Score = src.HomeScore ?? 0;
dest.HomeTeam.Scorers = ScorersForTeam(src, src.HomeTeamId);
PopulateRosterEligibility(dest.HomeTeam.Players, src.HomeTeam?.Players, tournamentId);
}
if (dest.VisitorTeam is not null)
{
dest.VisitorTeam.Score = src.VisitorScore ?? 0;
dest.VisitorTeam.Scorers = ScorersForTeam(src, src.VisitorTeamId);
PopulateRosterEligibility(dest.VisitorTeam.Players, src.VisitorTeam?.Players, tournamentId);
}
dest.TournamentId = tournamentId;
});
_ = CreateMap<Match, MinimalMatchResponse>()
.ForMember(dest => dest.MatchType, opt => opt.MapFrom(src => src.Type.ToString()))
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status.ToString()))
.ForMember(dest => dest.HomeTeamName, opt => opt.MapFrom(src => src.HomeTeam != null ? src.HomeTeam.Name : null))
.ForMember(dest => dest.VisitorTeamName, opt => opt.MapFrom(src => src.VisitorTeam != null ? src.VisitorTeam.Name : null))
.ForMember(dest => dest.WinningTeamName, opt => opt.MapFrom(src => src.WinningTeam != null ? src.WinningTeam.Name : null));
_ = CreateMap<UpdateMatchRequest, Match>();
}
/// <summary>
/// Builds the per-player scorer ranking for one team, attributing each scorer via Player.TeamId and summing points per player, highest first.
/// </summary>
private static List<ScorerByPlayerResponse> ScorersForTeam(Match match, Guid? teamId)
{
if (teamId is null)
{
return [];
}
Guid? tournamentId = match.Stage?.Division?.TournamentId;
return [.. match.Scorers
.Where(scorer => scorer.Player is not null && scorer.Player.TeamId == teamId.Value)
.GroupBy(scorer => scorer.PlayerId)
.Select(group => new ScorerByPlayerResponse
{
PlayerId = group.Key,
FullName = group.First().Player!.FullName,
JerseyNumber = JerseyNumberFor(group.First().Player!, tournamentId),
Points = group.Sum(scorer => scorer.Points),
})
.OrderByDescending(scorer => scorer.Points)];
}
/// <summary>
/// Resolves the player's jersey number from the matching roster registration for the tournament, since Player.JerseyNumber itself is transient.
/// </summary>
private static int? JerseyNumberFor(Player player, Guid? tournamentId)
{
if (player.PlayerTeamRegistrations is null || player.PlayerTeamRegistrations.Count == 0)
{
return null;
}
PlayerTeamRegistration? registration = tournamentId is null
? null
: player.PlayerTeamRegistrations.FirstOrDefault(reg => reg.TournamentId == tournamentId.Value);
return (registration ?? player.PlayerTeamRegistrations.First()).JerseyNumber;
}
/// <summary>
/// Fills each roster player's season-scoped medical record status, habilitado flag, and jersey number from the matching PlayerTeamRegistration, since the plain Player to PublicPlayerResponse map cannot resolve them on its own.
/// </summary>
private static void PopulateRosterEligibility(
List<PublicPlayerResponse> destPlayers, ICollection<Player>? srcPlayers, Guid? tournamentId)
{
if (tournamentId is null || srcPlayers is null || srcPlayers.Count == 0)
{
return;
}
Dictionary<Guid, Player> srcById = srcPlayers.ToDictionary(player => player.Id);
foreach (PublicPlayerResponse destPlayer in destPlayers)
{
if (!srcById.TryGetValue(destPlayer.Id, out Player? srcPlayer)
|| srcPlayer.PlayerTeamRegistrations is null)
{
continue;
}
PlayerTeamRegistration? registration = srcPlayer.PlayerTeamRegistrations
.FirstOrDefault(reg => reg.TournamentId == tournamentId.Value);
if (registration is null)
{
continue;
}
destPlayer.MedicalRecordStatus = registration.MedicalRecordStatus;
destPlayer.IsHabilitado = registration.IsHabilitado;
destPlayer.JerseyNumber = registration.JerseyNumber;
}
}
}
@@ -0,0 +1,26 @@
using Application.DTOs.Match.Response;
using Application.DTOs.MatchSeries.Response;
using AutoMapper;
using Domain.Entities.Models;
using System.Linq;
namespace API.AutoMapperProfiles;
public class MatchSeriesProfile : Profile
{
public MatchSeriesProfile()
{
_ = CreateMap<MatchSeries, MatchSeriesResponse>()
.ForMember(dest => dest.HomeTeamName, opt => opt.MapFrom(src => src.HomeTeam != null ? src.HomeTeam.Name : string.Empty))
.ForMember(dest => dest.VisitorTeamName, opt => opt.MapFrom(src => src.VisitorTeam != null ? src.VisitorTeam.Name : string.Empty))
.ForMember(dest => dest.WinningTeamName, opt => opt.MapFrom(src => src.WinningTeam != null ? src.WinningTeam.Name : null))
.ForMember(dest => dest.Games, opt => opt.MapFrom(src => src.Matches.OrderBy(m => m.GameNumber)));
_ = CreateMap<Match, SeriesGameResponse>()
.IncludeBase<Match, MinimalMatchResponse>()
.ForMember(dest => dest.GameNumber, opt => opt.MapFrom(src => src.GameNumber ?? 0));
}
}
@@ -0,0 +1,37 @@
using Application.DTOs.Abstract.Response;
using AutoMapper;
using System.Collections.Generic;
namespace API.AutoMapperProfiles;
/// <summary>
/// Converts a paginated response's items from the source entity type to the destination DTO type while preserving its paging metadata.
/// </summary>
/// <typeparam name="TSource">The source item type.</typeparam>
/// <typeparam name="TDestination">The destination item type.</typeparam>
public class PaginatedResponseConverter<TSource, TDestination>
: ITypeConverter<PaginatedResponse<TSource>, PaginatedResponse<TDestination>>
{
/// <summary>
/// Maps the source paginated response's items to the destination type.
/// </summary>
/// <param name="source">The paginated response being converted.</param>
/// <param name="destination">The unused destination instance supplied by AutoMapper.</param>
/// <param name="context">The mapping context used to convert the items.</param>
/// <returns>A new paginated response with mapped items and the source paging metadata.</returns>
public PaginatedResponse<TDestination> Convert(
PaginatedResponse<TSource> source,
PaginatedResponse<TDestination> destination,
ResolutionContext context)
{
return new()
{
Page = source.Page,
PageSize = source.PageSize,
TotalCount = source.TotalCount,
Items = context.Mapper.Map<List<TDestination>>(source.Items)
};
}
}
@@ -0,0 +1,14 @@
using Application.DTOs.Abstract.Response;
using AutoMapper;
namespace API.AutoMapperProfiles;
public class PaginatedResponseProfile : Profile
{
public PaginatedResponseProfile()
{
CreateMap(typeof(PaginatedResponse<>), typeof(PaginatedResponse<>))
.ConvertUsing(typeof(PaginatedResponseConverter<,>));
}
}
@@ -0,0 +1,27 @@
using Application.DTOs.Player.Request;
using Application.DTOs.Player.Response;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class PlayerProfile : Profile
{
public PlayerProfile()
{
_ = CreateMap<Player, PublicPlayerResponse>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.FullName))
.ReverseMap();
_ = CreateMap<Player, AdminPlayerResponse>()
.IncludeBase<Player, PublicPlayerResponse>();
_ = CreateMap<CreatePlayerRequest, Player>();
_ = CreateMap<UpdatePlayerRequest, Player>();
_ = CreateMap<PlayerTeamRegistration, PlayerRegistrationResponse>();
}
}
@@ -0,0 +1,29 @@
using Application.DTOs.PlayerSanction.Request;
using Application.DTOs.PlayerSanction.Response;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class PlayerSanctionProfile : Profile
{
public PlayerSanctionProfile()
{
_ = CreateMap<CreatePlayerSanctionRequest, PlayerSanction>();
_ = CreateMap<PlayerSanction, PlayerSanctionResponse>()
.ForMember(dest => dest.PlayerFullName,
opt => opt.MapFrom(src => src.Player != null ? src.Player.FullName : null))
.ForMember(dest => dest.TeamName,
opt => opt.MapFrom(src => src.Team != null ? src.Team.Name : null))
// FechasRemaining and IsActive are computed against finished rounds and populated by the controller, not mapped here.
.ForMember(dest => dest.FechasRemaining, opt => opt.Ignore())
.ForMember(dest => dest.IsActive, opt => opt.Ignore())
.ReverseMap()
.ForMember(dest => dest.Player, opt => opt.Ignore())
.ForMember(dest => dest.Team, opt => opt.Ignore());
_ = CreateMap<UpdatePlayerSanctionRequest, PlayerSanction>();
}
}
@@ -0,0 +1,24 @@
using Application.DTOs.PlayerStatistic.Request;
using Application.DTOs.PlayerStatistic.Response;
using AutoMapper;
using Domain.Entities.Models;
using System;
namespace API.AutoMapperProfiles;
public class PlayerStatisticProfile : Profile
{
public PlayerStatisticProfile()
{
_ = CreateMap<CreatePlayerStatisticRequest, PlayerStatistic>();
_ = CreateMap<PlayerStatistic, PlayerStatisticResponse>()
.ForMember(dest => dest.MatchDate, opt => opt.MapFrom(src => src.Match != null ? (DateTime?) src.Match.MatchDate : null))
.ReverseMap();
_ = CreateMap<UpdatePlayerStatisticRequest, PlayerStatistic>();
}
}
@@ -0,0 +1,21 @@
using Application.DTOs.PointDeductions.Request;
using Application.DTOs.PointDeductions.Response;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class PointDeductionProfile : Profile
{
public PointDeductionProfile()
{
_ = CreateMap<CreatePointDeductionRequest, TeamPointDeduction>();
_ = CreateMap<TeamPointDeduction, PointDeductionResponse>()
.ForMember(
dest => dest.TeamName,
opt => opt.MapFrom(src => src.Team != null ? src.Team.Name : null));
}
}
@@ -0,0 +1,22 @@
using Application.DTOs.Divisions.Response;
using Application.DTOs.PointDeductions.Response;
using Application.Utils.Helper.Standings;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class PositionProfile : Profile
{
public PositionProfile()
{
_ = CreateMap<Position, PositionResponse>();
_ = CreateMap<AppliedPointDeduction, AppliedPointDeductionResponse>();
// The nested Position to PositionResponse mapping above is applied automatically to each group's Positions collection.
_ = CreateMap<GroupStandings, GroupStandingsResponse>();
}
}
@@ -0,0 +1,22 @@
using Application.DTOs.Season.Request;
using Application.DTOs.Season.Response;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class SeasonProfile : Profile
{
public SeasonProfile()
{
_ = CreateMap<Season, SeasonResponse>();
_ = CreateMap<Tournament, SeasonTournamentResponse>();
_ = CreateMap<CreateSeasonRequest, Season>();
_ = CreateMap<UpdateSeasonRequest, Season>();
}
}
@@ -0,0 +1,24 @@
using Application.DTOs.Stage.Request;
using Application.DTOs.Stage.Response;
using Application.DTOs.Tournament.Response;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class StageProfile : Profile
{
public StageProfile()
{
_ = CreateMap<CreateStageRequest, Stage>();
_ = CreateMap<Stage, StageResponse>()
.ReverseMap();
_ = CreateMap<UpdateStageRequest, Stage>();
// Tournament cloning (HU-cloning): additive, name-convention mapping —
// carries no dates, no DrawnAt, no match data.
_ = CreateMap<Stage, StageStructureResponse>();
}
}
@@ -0,0 +1,31 @@
using Application.DTOs.Team.Request;
using Application.DTOs.Team.Response;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class TeamProfile : Profile
{
public TeamProfile()
{
_ = CreateMap<Team, TeamResponse>()
.ForMember(dest => dest.ClubId, opt => opt.MapFrom(src => src.ClubId))
.ForMember(dest => dest.TournamentName, opt => opt.MapFrom(src => src.Tournament != null ? src.Tournament.Name : null))
.ReverseMap();
_ = CreateMap<Team, TeamDetailedMatchResponse>()
.ReverseMap();
_ = CreateMap<CreateTeamRequest, Team>()
.ForMember(dest => dest.ThreeLetterCode, opt => opt.MapFrom(src => src.ThreeLetterCode.ToUpper()));
_ = CreateMap<UpdateTeamRequest, Team>()
.ForMember(dest => dest.ThreeLetterCode, opt => opt.MapFrom(src => src.ThreeLetterCode != null ? src.ThreeLetterCode.ToUpper() : null))
.ForMember(dest => dest.JerseyStyle, opt => opt.Condition(src => src.JerseyStyle != null))
.ForMember(dest => dest.ShirtSecondaryColor, opt => opt.Condition(src => src.ShirtSecondaryColor != null))
.ForMember(dest => dest.ShirtTertiaryColor, opt => opt.Condition(src => src.ShirtTertiaryColor != null));
}
}
@@ -0,0 +1,24 @@
using Application.DTOs.TeamStaff.Request;
using Application.DTOs.TeamStaff.Response;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class TeamStaffProfile : Profile
{
public TeamStaffProfile()
{
_ = CreateMap<CreateTeamStaffRequest, TeamStaff>();
_ = CreateMap<TeamStaff, TeamStaffResponse>()
.ForMember(
dest => dest.Role,
opt => opt.MapFrom(src => src.Role.ToString()))
.ForMember(
dest => dest.TeamName,
opt => opt.MapFrom(src => src.Team != null ? src.Team.Name : null));
}
}
@@ -0,0 +1,36 @@
using Application.DTOs.Tournament.Request;
using Application.DTOs.Tournament.Response;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class TournamentProfile : Profile
{
public TournamentProfile()
{
_ = CreateMap<Tournament, TournamentResponse>()
.ForMember(dest => dest.Divisions, opt => opt.MapFrom(src => src.Divisions))
// SeasonId flows by name convention; SeasonName is resolved from the Season navigation, degrading to null when the tournament is ungrouped or the season was not included.
.ForMember(dest => dest.SeasonName, opt => opt.MapFrom(src => src.Season != null ? src.Season.Name : null))
.ForMember(dest => dest.SeasonSlug, opt => opt.MapFrom(src => src.Season != null ? src.Season.Slug : null))
.ReverseMap();
// Tournament cloning (HU-cloning): the structure-tree read is additive and
// maps by name convention only — Divisions resolves to DivisionStructureResponse
// once DivisionProfile registers that map, carrying no instance data.
_ = CreateMap<Tournament, TournamentStructureResponse>();
_ = CreateMap<CreateTournamentRequest, Tournament>();
_ = CreateMap<UpdateTournamentRequest, Tournament>()
// Status is intentionally not mapped here since a status change is a guarded state-machine transition with fixture side effects driven through TournamentService.ChangeStatusAsync, never a blind field overwrite from a generic update.
.ForMember(dest => dest.Status, opt => opt.Ignore())
// Category is not mapped on update since a tournament's gender category is fixed at creation; flipping it later would silently mix it with divisions already created under the original category.
.ForMember(dest => dest.Category, opt => opt.Ignore())
// StartDate is fixed at creation since it drives when the tournament is understood to have happened for season grouping and champions history, so it must never move after the fact regardless of status.
.ForMember(dest => dest.StartDate, opt => opt.Ignore());
}
}
@@ -0,0 +1,21 @@
using Application.DTOs.Venue.Request;
using Application.DTOs.Venue.Response;
using AutoMapper;
using Domain.Entities.Models;
namespace API.AutoMapperProfiles;
public class VenueProfile : Profile
{
public VenueProfile()
{
_ = CreateMap<Venue, VenueResponse>()
.ReverseMap();
_ = CreateMap<CreateVenueRequest, Venue>();
_ = CreateMap<UpdateVenueRequest, Venue>();
}
}