Files
vtuber-awards/Backend/Endpoints/AdminSeasonManagementSupport.cs
T

314 lines
11 KiB
C#

using Backend.Common;
using Backend.Contracts;
using Backend.Data;
using Microsoft.EntityFrameworkCore;
namespace Backend.Endpoints;
public static partial class AdminSeasonManagementEndpoints
{
private const int MaxCategoryGroupNameLength = 80;
private const int MaxCategoryNameLength = 120;
private const int MaxCategorySlugLength = 120;
private const int MaxCategoryDescriptionLength = 600;
private const int MaxCandidateDisplayNameLength = 120;
private const int MaxCandidateChannelSlugLength = 120;
private const int MaxCandidatePlatformLength = 60;
private static IResult? ValidateSeasonRequest(CreateSeasonRequest request)
{
if (request.Year < 2020 || request.Year > 2100)
{
return Results.BadRequest(new { message = "Please provide a valid award year." });
}
if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.CurrentPhase))
{
return Results.BadRequest(new { message = "Season name and current phase are required." });
}
if (!IsKnownSeasonPhase(request.CurrentPhase))
{
return Results.BadRequest(new { message = "Current phase must be nomination, voting, review, show, or completed." });
}
if (!SeasonMappings.IsSeasonScheduleValid(
request.NominationStartsAt,
request.NominationEndsAt,
request.VotingStartsAt,
request.VotingEndsAt,
request.ReviewStartsAt,
request.ReviewEndsAt,
request.ShowDate))
{
return Results.BadRequest(new { message = "The season schedule is not in chronological order." });
}
return null;
}
private static IResult? ValidateSeasonRequest(UpdateSeasonRequest request)
{
if (request.Year < 2020 || request.Year > 2100)
{
return Results.BadRequest(new { message = "Please provide a valid award year." });
}
if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.CurrentPhase))
{
return Results.BadRequest(new { message = "Season name and current phase are required." });
}
if (!IsKnownSeasonPhase(request.CurrentPhase))
{
return Results.BadRequest(new { message = "Current phase must be nomination, voting, review, show, or completed." });
}
if (!SeasonMappings.IsSeasonScheduleValid(
request.NominationStartsAt,
request.NominationEndsAt,
request.VotingStartsAt,
request.VotingEndsAt,
request.ReviewStartsAt,
request.ReviewEndsAt,
request.ShowDate))
{
return Results.BadRequest(new { message = "The season schedule is not in chronological order." });
}
return null;
}
private static string NormalizeSeasonStreamUrl(string? showStreamUrl)
{
return SeasonMappings.NormalizeSeasonStreamUrl(showStreamUrl);
}
private static bool IsKnownSeasonPhase(string? currentPhase)
{
var value = currentPhase?.Trim().ToLowerInvariant() ?? string.Empty;
return value.Contains("show")
|| value.Contains("abgeschlossen")
|| value.Contains("archiv")
|| value.Contains("complete")
|| value.Contains("ended")
|| value.Contains("review")
|| value.Contains("auswert")
|| value.Contains("vot")
|| value.Contains("nomin");
}
private static async Task UnsetOtherCurrentSeasonsAsync(
AwardsDbContext db,
bool shouldUnsetOthers,
int? seasonIdToKeep,
CancellationToken cancellationToken)
{
if (!shouldUnsetOthers)
{
return;
}
var activeSeasons = await db.Seasons
.Where(item => item.IsCurrent && (!seasonIdToKeep.HasValue || item.Id != seasonIdToKeep.Value))
.ToListAsync(cancellationToken);
foreach (var activeSeason in activeSeasons)
{
activeSeason.IsCurrent = false;
}
}
private static IResult CreateReadinessError(IEnumerable<string> issues)
{
var issueList = issues.ToArray();
return Results.BadRequest(new
{
message = $"Public-/Archiv-Readiness blockiert: {string.Join(" ", issueList)}",
issues = issueList,
});
}
private static string[] BuildNewSeasonReadinessIssues(
string currentPhase,
bool isCurrent,
int copiedCategoryCount)
{
var phaseKey = SeasonMappings.NormalizePhaseKey(currentPhase);
var needsCandidateReadiness = RequiresCandidateReadiness(phaseKey, isCurrent);
var needsWinnerReadiness = RequiresWinnerReadiness(phaseKey);
if (!isCurrent && !needsWinnerReadiness)
{
return [];
}
var issues = new List<string>();
if (copiedCategoryCount <= 0)
{
issues.Add("Mindestens eine Kategorie ist erforderlich.");
}
if (needsCandidateReadiness)
{
issues.Add("Kandidaten muessen vor dieser Phase fuer alle Kategorien gepflegt sein.");
}
if (needsWinnerReadiness)
{
issues.Add("Abgeschlossen ist erst moeglich, wenn jede Kategorie einen Gewinner hat.");
}
return issues.ToArray();
}
private static async Task<string[]> BuildSeasonReadinessIssuesAsync(
AwardsDbContext db,
int seasonId,
string currentPhase,
bool isCurrent,
CancellationToken cancellationToken)
{
var phaseKey = SeasonMappings.NormalizePhaseKey(currentPhase);
var needsCandidateReadiness = RequiresCandidateReadiness(phaseKey, isCurrent);
var needsWinnerReadiness = RequiresWinnerReadiness(phaseKey);
if (!isCurrent && !needsWinnerReadiness)
{
return [];
}
var categoryIds = await db.Categories
.AsNoTracking()
.Where(item => item.SeasonId == seasonId)
.Select(item => item.Id)
.ToArrayAsync(cancellationToken);
var issues = new List<string>();
if (categoryIds.Length == 0)
{
issues.Add("Mindestens eine Kategorie ist erforderlich.");
}
if (needsCandidateReadiness && categoryIds.Length > 0)
{
var categoriesWithCandidates = await db.Candidates
.AsNoTracking()
.Where(item => item.SeasonId == seasonId)
.Select(item => item.CategoryId)
.Distinct()
.CountAsync(cancellationToken);
var emptyCategories = Math.Max(0, categoryIds.Length - categoriesWithCandidates);
if (emptyCategories > 0)
{
issues.Add($"{emptyCategories} Kategorien haben noch keine Kandidaten.");
}
}
if (needsWinnerReadiness && categoryIds.Length > 0)
{
var categoriesWithResults = await db.Results
.AsNoTracking()
.Where(item => item.SeasonId == seasonId)
.Select(item => item.CategoryId)
.Distinct()
.CountAsync(cancellationToken);
var missingResults = Math.Max(0, categoryIds.Length - categoriesWithResults);
if (missingResults > 0)
{
issues.Add($"{missingResults} Kategorien haben noch keinen Gewinner.");
}
}
return issues.ToArray();
}
private static bool RequiresCandidateReadiness(string phaseKey, bool isCurrent)
{
return (isCurrent && !string.Equals(phaseKey, "nomination", StringComparison.Ordinal))
|| string.Equals(phaseKey, "completed", StringComparison.Ordinal);
}
private static bool RequiresWinnerReadiness(string phaseKey)
{
return string.Equals(phaseKey, "completed", StringComparison.Ordinal);
}
private static IResult? ValidateCategoryRequest(UpsertCategoryRequest request)
{
var groupName = request.GroupName.Trim();
var name = request.Name.Trim();
var slug = request.Slug.Trim();
var description = request.Description.Trim();
if (string.IsNullOrWhiteSpace(groupName) || groupName.Length > MaxCategoryGroupNameLength)
{
return Results.BadRequest(new { message = $"Category group name is required and must stay below {MaxCategoryGroupNameLength} characters." });
}
if (string.IsNullOrWhiteSpace(name) || name.Length > MaxCategoryNameLength)
{
return Results.BadRequest(new { message = $"Category name is required and must stay below {MaxCategoryNameLength} characters." });
}
if (string.IsNullOrWhiteSpace(slug) || slug.Length > MaxCategorySlugLength)
{
return Results.BadRequest(new { message = $"Category slug is required and must stay below {MaxCategorySlugLength} characters." });
}
if (!slug.All(value => char.IsLetterOrDigit(value) || value is '-' or '_'))
{
return Results.BadRequest(new { message = "Category slug contains unsupported characters." });
}
if (description.Length > MaxCategoryDescriptionLength)
{
return Results.BadRequest(new { message = $"Category description must stay below {MaxCategoryDescriptionLength} characters." });
}
if (request.SortOrder is < 0 or > 500)
{
return Results.BadRequest(new { message = "Category sort order must be between 0 and 500." });
}
if (request.MaxNomineesPerUser is < 1 or > 10)
{
return Results.BadRequest(new { message = "Max nominees per user must be between 1 and 10." });
}
return null;
}
private static IResult? ValidateCandidateRequest(UpsertCandidateRequest request)
{
var displayName = request.DisplayName.Trim();
var channelSlug = request.ChannelSlug.Trim();
var platform = request.Platform.Trim();
if (request.CategoryId <= 0)
{
return Results.BadRequest(new { message = "A valid category is required." });
}
if (string.IsNullOrWhiteSpace(displayName) || displayName.Length > MaxCandidateDisplayNameLength)
{
return Results.BadRequest(new { message = $"Display name is required and must stay below {MaxCandidateDisplayNameLength} characters." });
}
if (string.IsNullOrWhiteSpace(channelSlug) || channelSlug.Length > MaxCandidateChannelSlugLength)
{
return Results.BadRequest(new { message = $"Channel slug is required and must stay below {MaxCandidateChannelSlugLength} characters." });
}
if (!channelSlug.All(value => char.IsLetterOrDigit(value) || value is '-' or '_' or '.'))
{
return Results.BadRequest(new { message = "Channel slug contains unsupported characters." });
}
if (string.IsNullOrWhiteSpace(platform) || platform.Length > MaxCandidatePlatformLength)
{
return Results.BadRequest(new { message = $"Platform is required and must stay below {MaxCandidatePlatformLength} characters." });
}
return null;
}
}