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,261 @@
using Domain.Entities.Models;
using Domain.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Application.Utils.Helper.Champions;
/// <summary>
/// Pure logic that resolves a division's podium from its elimination bracket, with no data access.
/// </summary>
public static class ChampionResolver
{
/// <summary>
/// A team occupying a podium place.
/// </summary>
public sealed record TeamRef(Guid TeamId, string TeamName, string LogoUrl);
/// <summary>
/// The three podium places; any place may be null when undecided.
/// </summary>
public sealed class Podium
{
public TeamRef? First { get; init; }
public TeamRef? Second { get; init; }
public TeamRef? Third { get; init; }
/// <summary>
/// True when Third is null only because the top cup never had a real bracket round to draw a third place from.
/// </summary>
public bool ImplicitThirdFromStandings { get; init; }
}
private static readonly HashSet<StageType> BracketDepthStageTypes =
[
StageType.RoundOf16,
StageType.QuarterFinal,
StageType.SemiFinal,
];
/// <summary>
/// The champion of a single sub-cup, a playoff bracket, of a division.
/// </summary>
public sealed record CupChampion(string? CupName, int SeedOrder, TeamRef Champion);
/// <summary>
/// Resolves the podium of a division that has a playoff.
/// </summary>
/// <param name="eliminationStages">Every non-group stage of the division.</param>
/// <param name="mappings">The division's position-range to cup mappings, which may be empty.</param>
/// <param name="eliminationMatches">The finished/seeded matches of the elimination stages, with their team navigations loaded.</param>
/// <param name="series">The division's best-of-N series, with their team navigations loaded.</param>
public static Podium ResolvePlayoffPodium(
IReadOnlyList<Stage> eliminationStages,
IReadOnlyList<DivisionPlayoffMapping> mappings,
IReadOnlyList<Match> eliminationMatches,
IReadOnlyList<MatchSeries> series)
{
string? topBracket = ResolveTopBracketName(eliminationStages, mappings);
Stage? finalStage = eliminationStages
.Where(stage => stage.BracketName == topBracket && stage.StageType == StageType.Final)
.OrderBy(stage => stage.Order)
.FirstOrDefault();
Stage? thirdPlaceStage = eliminationStages
.Where(stage => stage.BracketName == topBracket && stage.StageType == StageType.ThirdPlace)
.OrderBy(stage => stage.Order)
.FirstOrDefault();
Dictionary<Guid, TeamRef> teamsById = BuildTeamLookup(eliminationMatches, series);
(Guid Winner, Guid Loser)? finalOutcome = finalStage is null
? null
: ResolveStageOutcome(finalStage, eliminationMatches, series);
(Guid Winner, Guid Loser)? thirdOutcome = thirdPlaceStage is null
? null
: ResolveStageOutcome(thirdPlaceStage, eliminationMatches, series);
TeamRef? third = thirdOutcome is null ? null : Lookup(teamsById, thirdOutcome.Value.Winner);
bool hasBracketDepth = eliminationStages
.Any(stage => stage.BracketName == topBracket && BracketDepthStageTypes.Contains(stage.StageType));
return new Podium
{
First = finalOutcome is null ? null : Lookup(teamsById, finalOutcome.Value.Winner),
Second = finalOutcome is null ? null : Lookup(teamsById, finalOutcome.Value.Loser),
Third = third,
ImplicitThirdFromStandings = third is null && !hasBracketDepth,
};
}
/// <summary>
/// Resolves the champion of every sub-cup, a playoff bracket, of a division, not just the top one.
/// </summary>
public static IReadOnlyList<CupChampion> ResolveCupChampions(
IReadOnlyList<Stage> eliminationStages,
IReadOnlyList<DivisionPlayoffMapping> mappings,
IReadOnlyList<Match> eliminationMatches,
IReadOnlyList<MatchSeries> series)
{
List<string?> bracketNames = [.. eliminationStages
.Select(stage => stage.BracketName)
.Distinct()];
bool singleBracket = bracketNames.Count <= 1;
Dictionary<Guid, TeamRef> teamsById = BuildTeamLookup(eliminationMatches, series);
// The cup receiving the lowest FromPosition is the top seed tier; brackets unnamed by any mapping sort after, by name.
Dictionary<string, int> seedByBracket = mappings
.GroupBy(mapping => mapping.Destination)
.ToDictionary(group => group.Key, group => group.Min(m => m.FromPosition));
List<CupChampion> champions = [];
foreach (string? bracketName in bracketNames)
{
Stage? finalStage = eliminationStages
.Where(stage => stage.BracketName == bracketName && stage.StageType == StageType.Final)
.OrderBy(stage => stage.Order)
.FirstOrDefault();
if (finalStage is null)
{
continue;
}
(Guid Winner, Guid Loser)? outcome = ResolveStageOutcome(finalStage, eliminationMatches, series);
TeamRef? champion = outcome is null ? null : Lookup(teamsById, outcome.Value.Winner);
if (champion is null)
{
continue;
}
int seedOrder = bracketName is not null && seedByBracket.TryGetValue(bracketName, out int seed)
? seed
: int.MaxValue;
champions.Add(new CupChampion(singleBracket ? null : bracketName, seedOrder, champion));
}
return [.. champions
.OrderBy(c => c.SeedOrder)
.ThenBy(c => c.CupName, StringComparer.Ordinal)];
}
/// <summary>
/// Picks the division's top cup name.
/// </summary>
private static string? ResolveTopBracketName(
IReadOnlyList<Stage> eliminationStages,
IReadOnlyList<DivisionPlayoffMapping> mappings)
{
List<string?> bracketNames = [.. eliminationStages
.Select(stage => stage.BracketName)
.Distinct()];
if (bracketNames.Count <= 1)
{
return bracketNames.FirstOrDefault();
}
DivisionPlayoffMapping? topMapping = mappings
.OrderBy(mapping => mapping.FromPosition)
.FirstOrDefault();
if (topMapping is not null && bracketNames.Contains(topMapping.Destination))
{
return topMapping.Destination;
}
return bracketNames
.Where(name => name is not null)
.OrderBy(name => name, StringComparer.Ordinal)
.FirstOrDefault() ?? bracketNames[0];
}
/// <summary>
/// Reads a single elimination stage's decided outcome as a winner and loser pair.
/// </summary>
private static (Guid Winner, Guid Loser)? ResolveStageOutcome(
Stage stage,
IReadOnlyList<Match> eliminationMatches,
IReadOnlyList<MatchSeries> series)
{
List<MatchSeries> stageSeries = [.. series.Where(s => s.StageId == stage.Id)];
if (stageSeries.Count > 0)
{
MatchSeries? decided = stageSeries.Find(s => s.WinningTeamId.HasValue);
if (decided is null)
{
return null;
}
Guid seriesWinner = decided.WinningTeamId!.Value;
Guid seriesLoser = seriesWinner == decided.HomeTeamId ? decided.VisitorTeamId : decided.HomeTeamId;
return (seriesWinner, seriesLoser);
}
Match? match = eliminationMatches
.Where(m => m.StageId == stage.Id
&& m.IsFinished
&& m.WinningTeamId.HasValue
&& m.HomeTeamId.HasValue
&& m.VisitorTeamId.HasValue)
.OrderByDescending(m => m.MatchDate)
.FirstOrDefault();
if (match is null)
{
return null;
}
Guid winner = match.WinningTeamId!.Value;
Guid loser = winner == match.HomeTeamId!.Value ? match.VisitorTeamId!.Value : match.HomeTeamId!.Value;
return (winner, loser);
}
private static Dictionary<Guid, TeamRef> BuildTeamLookup(
IReadOnlyList<Match> eliminationMatches,
IReadOnlyList<MatchSeries> series)
{
Dictionary<Guid, TeamRef> teamsById = [];
foreach (Match match in eliminationMatches)
{
AddTeam(teamsById, match.HomeTeam);
AddTeam(teamsById, match.VisitorTeam);
AddTeam(teamsById, match.WinningTeam);
}
foreach (MatchSeries s in series)
{
AddTeam(teamsById, s.HomeTeam);
AddTeam(teamsById, s.VisitorTeam);
AddTeam(teamsById, s.WinningTeam);
}
return teamsById;
}
private static void AddTeam(Dictionary<Guid, TeamRef> teamsById, Team? team)
{
if (team is null || teamsById.ContainsKey(team.Id))
{
return;
}
teamsById[team.Id] = new TeamRef(team.Id, team.Name, team.LogoUrl);
}
private static TeamRef? Lookup(Dictionary<Guid, TeamRef> teamsById, Guid teamId)
{
return teamsById.TryGetValue(teamId, out TeamRef? team) ? team : null;
}
}