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,56 @@
using Application.Utils.Constants;
using Domain.Entities.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Application.Utils.Helper.Playoff;
/// <summary>
/// Resolves the single bracket seed order for a multi-group cross-division cup.
/// </summary>
public static class CrossCupGroupSeeder
{
/// <summary>
/// Pools the top qualifiersPerGroup teams of each group's standings and returns them as one ordered seed list, best seed first, keeping each team's full Position (not just its id) so a caller can still apply position-range cup mappings over the pooled order.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when qualifiersPerGroup is less than 1, or when fewer than two teams are pooled across all groups, since a bracket needs at least two seeds.
/// </exception>
public static List<Position> PoolAndOrder(
IEnumerable<IReadOnlyList<Position>> groupStandings,
int qualifiersPerGroup)
{
if (qualifiersPerGroup < 1)
{
throw new InvalidOperationException(ErrorMessages.Playoff.InvalidQualifiersPerGroup(qualifiersPerGroup));
}
List<Position> pooled = [.. groupStandings.SelectMany(standings => standings.Take(qualifiersPerGroup))];
if (pooled.Count < 2)
{
throw new InvalidOperationException(ErrorMessages.Playoff.NotEnoughRankedTeams);
}
return [.. pooled
.OrderByDescending(position => position.Points)
.ThenByDescending(position => position.Wins)
.ThenByDescending(position => position.PointsDifference)
.ThenByDescending(position => position.PointsFor)
.ThenBy(position => position.TeamId)];
}
/// <summary>
/// Pools the top qualifiersPerGroup teams of each group's standings and returns them as one ordered seed list of team ids, best seed first.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when qualifiersPerGroup is less than 1, or when fewer than two teams are pooled across all groups, since a bracket needs at least two seeds.
/// </exception>
public static List<Guid> ResolveSeedOrder(
IEnumerable<IReadOnlyList<Position>> groupStandings,
int qualifiersPerGroup) =>
[.. PoolAndOrder(groupStandings, qualifiersPerGroup).Select(position => position.TeamId)];
}
@@ -0,0 +1,49 @@
using Application.Utils.Constants;
using Domain.Entities.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Application.Utils.Helper.Playoff;
/// <summary>
/// Validates a division's position-range to playoff-destination mappings.
/// </summary>
public static class PlayoffMappingValidator
{
/// <summary>
/// Throws InvalidOperationException if any range is malformed or if two ranges overlap.
/// </summary>
public static void Validate(IEnumerable<DivisionPlayoffMapping> mappings)
{
List<DivisionPlayoffMapping> ordered = [.. mappings.OrderBy(m => m.FromPosition)];
foreach (DivisionPlayoffMapping mapping in ordered)
{
if (mapping.FromPosition < 1 || mapping.ToPosition < mapping.FromPosition)
{
throw new InvalidOperationException(
ErrorMessages.Playoff.InvalidRange(mapping.FromPosition, mapping.ToPosition));
}
if (string.IsNullOrWhiteSpace(mapping.Destination))
{
throw new InvalidOperationException(ErrorMessages.Playoff.EmptyDestination);
}
}
for (int i = 1; i < ordered.Count; i++)
{
// Ordered by FromPosition, so an overlap exists when the current range starts at or before the previous range's end.
if (ordered[i].FromPosition <= ordered[i - 1].ToPosition)
{
throw new InvalidOperationException(
ErrorMessages.Playoff.OverlappingRanges(
ordered[i - 1].FromPosition, ordered[i - 1].ToPosition,
ordered[i].FromPosition, ordered[i].ToPosition));
}
}
}
}
@@ -0,0 +1,59 @@
using Domain.Entities.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Application.Utils.Helper.Playoff;
/// <summary>
/// Resolves which teams qualify for each playoff cup from the final group-stage standings and the per-division position-range mappings.
/// </summary>
public static class PlayoffQualificationResolver
{
/// <summary>
/// One division's contribution to the playoffs: its final standings, best team first, and its position-range to destination mappings.
/// </summary>
public sealed class DivisionStandings
{
public required IReadOnlyList<Position> Standings { get; init; }
public required IReadOnlyList<DivisionPlayoffMapping> Mappings { get; init; }
public int DivisionOrder { get; init; }
}
/// <summary>
/// Returns, per destination cup, the ordered list of qualifying team ids gathered across every contributing division, best seed first.
/// </summary>
public static Dictionary<string, List<Guid>> Resolve(IEnumerable<DivisionStandings> divisions)
{
List<(string Destination, int Position, int DivisionOrder, Guid TeamId)> qualifiers = [];
foreach (DivisionStandings division in divisions)
{
for (int i = 0; i < division.Standings.Count; i++)
{
int position = i + 1;
DivisionPlayoffMapping? mapping = division.Mappings
.FirstOrDefault(m => position >= m.FromPosition && position <= m.ToPosition);
if (mapping is null)
{
continue;
}
qualifiers.Add((mapping.Destination, position, division.DivisionOrder, division.Standings[i].TeamId));
}
}
return qualifiers
.GroupBy(q => q.Destination)
.ToDictionary(
group => group.Key,
group => group
.OrderBy(q => q.Position)
.ThenBy(q => q.DivisionOrder)
.Select(q => q.TeamId)
.ToList());
}
}
@@ -0,0 +1,80 @@
using Application.Utils.Constants;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Application.Utils.Helper.Playoff;
/// <summary>
/// Pairs a ranked list of teams into first-round playoff matchups using the standard bracket seeding order.
/// </summary>
public static class PlayoffSeeder
{
/// <summary>
/// Builds the first-round pairs for a ranked list of teams, best seed first.
/// </summary>
public static List<(Guid HomeTeamId, Guid? VisitorTeamId)> SeedPairs(IReadOnlyList<Guid> orderedTeamIds)
{
int count = orderedTeamIds.Count;
if (count < 2)
{
throw new InvalidOperationException(ErrorMessages.Playoff.NotEnoughRankedTeams);
}
int bracketSize = NextPowerOfTwo(count);
List<Guid?> paddedSeeds = [.. orderedTeamIds.Select(id => (Guid?) id)];
paddedSeeds.AddRange(Enumerable.Repeat((Guid?) null, bracketSize - count));
List<int> bracketOrder = BuildSeedOrder(bracketSize);
List<(Guid, Guid?)> pairs = [];
for (int i = 0; i < bracketSize; i += 2)
{
Guid? first = paddedSeeds[bracketOrder[i] - 1];
Guid? second = paddedSeeds[bracketOrder[i + 1] - 1];
(Guid? home, Guid? visitor) = first is null ? (second, first) : (first, second);
pairs.Add((home!.Value, visitor));
}
return pairs;
}
/// <summary>
/// Smallest power of two greater than or equal to value, the bracket size a seed pool of that many teams pads up to.
/// </summary>
public static int NextPowerOfTwo(int value)
{
int power = 1;
while (power < value)
{
power *= 2;
}
return power;
}
/// <summary>
/// Recursively builds the classic bracket seed-position order.
/// </summary>
private static List<int> BuildSeedOrder(int size)
{
if (size == 1)
{
return [1];
}
List<int> previous = BuildSeedOrder(size / 2);
List<int> current = [];
foreach (int seed in previous)
{
current.Add(seed);
current.Add(size + 1 - seed);
}
return current;
}
}
@@ -0,0 +1,38 @@
using Application.DTOs.Divisions.Response;
using Domain.Entities.Models;
using System.Collections.Generic;
using System.Linq;
namespace Application.Utils.Helper.Playoff;
/// <summary>
/// Turns a division's playoff mappings into the ordered qualification ranges the public standings table paints.
/// </summary>
public static class QualificationRangeBuilder
{
/// <summary>
/// Builds the ordered qualification ranges for a set of playoff mappings.
/// </summary>
/// <param name="mappings">The division's playoff mappings, in any order.</param>
/// <returns>The ordered qualification ranges; empty when there are none.</returns>
public static List<QualificationRangeResponse> Build(IEnumerable<DivisionPlayoffMapping>? mappings)
{
if (mappings is null)
{
return [];
}
return [.. mappings
.OrderBy(mapping => mapping.FromPosition)
.ThenBy(mapping => mapping.ToPosition)
.Select((mapping, index) => new QualificationRangeResponse
{
FromPosition = mapping.FromPosition,
ToPosition = mapping.ToPosition,
CupName = mapping.Destination,
Order = index,
})];
}
}