Sube carpetas API, API.Tests, Application y Domain del Backend
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.2.0">
|
||||
<NoWarn>NU1903</NoWarn>
|
||||
</PackageReference>
|
||||
<PackageReference Include="LinqKit.Microsoft.EntityFrameworkCore" Version="8.1.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.12" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.22.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide single-flight guard for backup and restore write operations.
|
||||
/// </summary>
|
||||
public sealed class BackupOperationLock
|
||||
{
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to acquire the lock, waiting up to timeout; callers pass TimeSpan.Zero to fail fast.
|
||||
/// </summary>
|
||||
public Task<bool> WaitAsync(TimeSpan timeout, CancellationToken ct = default)
|
||||
{
|
||||
return _semaphore.WaitAsync(timeout, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the lock; must be called exactly once for every successful WaitAsync that returned true.
|
||||
/// </summary>
|
||||
public void Release()
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
using Application.DTOs.Backup.Response;
|
||||
using Application.Interfaces.Backup;
|
||||
using Application.Interfaces.Services;
|
||||
using Application.Utils.Constants;
|
||||
|
||||
using Domain.Constants;
|
||||
using Domain.Entities.Models;
|
||||
using Domain.Enums;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Application.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// The one shared write path for backup operations.
|
||||
/// </summary>
|
||||
public sealed class BackupOperationsService(
|
||||
IBackupCatalog catalog,
|
||||
IBackupStorage storage,
|
||||
IDatabaseBackupService backupService,
|
||||
IDatabaseRestoreService restoreService,
|
||||
IBackupRetentionPolicy retentionPolicy,
|
||||
BackupOptions options,
|
||||
BackupOperationLock operationLock,
|
||||
IMaintenanceModeState maintenanceModeState,
|
||||
IAuditService auditService,
|
||||
ILogger<BackupOperationsService> logger) : IBackupOperationsService
|
||||
{
|
||||
public Task<IReadOnlyList<BackupRecord>> ListNewestFirstAsync(CancellationToken ct = default) =>
|
||||
catalog.ListNewestFirstAsync(ct);
|
||||
|
||||
public async Task<BackupOperationResult> CreateBackupAsync(BackupOrigin origin, CancellationToken ct = default)
|
||||
{
|
||||
if (!await operationLock.WaitAsync(TimeSpan.Zero, ct))
|
||||
{
|
||||
return new BackupOperationResult(BackupOperationOutcome.Busy, null, ErrorMessages.Backup.OperationInProgress);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await CreateBackupCoreAsync(origin, applyRetention: true, ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
operationLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BackupOperationResult> DeleteBackupAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
if (!await operationLock.WaitAsync(TimeSpan.Zero, ct))
|
||||
{
|
||||
return new BackupOperationResult(BackupOperationOutcome.Busy, null, ErrorMessages.Backup.OperationInProgress);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
BackupRecord? record = await catalog.GetByIdAsync(id, ct);
|
||||
if (record is null)
|
||||
{
|
||||
return new BackupOperationResult(BackupOperationOutcome.NotFound, null, null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await storage.DeleteAsync(record.StoragePath, ct);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
// An already-missing file deleted out-of-band must not block removing the catalog row, so log and continue.
|
||||
logger.LogWarning(
|
||||
ex,
|
||||
"Backup file could not be deleted from storage; removing catalog record anyway. StoragePath: {StoragePath}",
|
||||
record.StoragePath);
|
||||
}
|
||||
|
||||
await catalog.RemoveAsync(record.Id, ct);
|
||||
|
||||
return new BackupOperationResult(BackupOperationOutcome.Completed, BackupRecordResponse.FromEntity(record), null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
operationLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enters maintenance mode, takes an automatic safety backup, restores the selected one, then exits.
|
||||
/// </summary>
|
||||
public async Task<BackupOperationResult> RestoreBackupAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
if (!await operationLock.WaitAsync(TimeSpan.Zero, ct))
|
||||
{
|
||||
return new BackupOperationResult(BackupOperationOutcome.Busy, null, ErrorMessages.Backup.OperationInProgress);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
BackupRecord? record = await catalog.GetByIdAsync(id, ct);
|
||||
if (record is null)
|
||||
{
|
||||
return new BackupOperationResult(BackupOperationOutcome.NotFound, null, null);
|
||||
}
|
||||
|
||||
maintenanceModeState.Enter($"Restoring backup {record.Id}.");
|
||||
string? tempFilePath = null;
|
||||
try
|
||||
{
|
||||
BackupOperationResult safetyBackup = await CreateBackupCoreAsync(BackupOrigin.Job, applyRetention: false, ct);
|
||||
if (safetyBackup.Outcome != BackupOperationOutcome.Completed)
|
||||
{
|
||||
return new BackupOperationResult(BackupOperationOutcome.Failed, null, safetyBackup.Message);
|
||||
}
|
||||
|
||||
tempFilePath = Path.Combine(Path.GetTempPath(), $"restore-{Guid.NewGuid():N}.sql");
|
||||
await using (Stream source = await storage.OpenReadAsync(record.StoragePath, ct))
|
||||
await using (FileStream destination = File.Create(tempFilePath))
|
||||
{
|
||||
await source.CopyToAsync(destination, ct);
|
||||
}
|
||||
|
||||
await restoreService.RestoreAsync(tempFilePath, ct);
|
||||
|
||||
// Logging the restore is non-throwing by contract via IAuditService.LogAsync, so a logging hiccup never turns a successful restore into a reported failure.
|
||||
await auditService.LogAsync(
|
||||
AuditAction.BackupRestore,
|
||||
targetType: nameof(BackupRecord),
|
||||
targetId: record.Id.ToString(),
|
||||
targetName: record.StoragePath,
|
||||
detail: $"Origen: {record.Origin}",
|
||||
ct: ct);
|
||||
|
||||
return new BackupOperationResult(BackupOperationOutcome.Completed, safetyBackup.Record, null);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(ex, "Restore failed; maintenance mode will be cleared and the host keeps running.");
|
||||
return new BackupOperationResult(BackupOperationOutcome.Failed, null, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (tempFilePath is not null && File.Exists(tempFilePath))
|
||||
{
|
||||
File.Delete(tempFilePath);
|
||||
}
|
||||
|
||||
maintenanceModeState.Exit();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
operationLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assumes the lock is already held by the caller, so a restore's safety backup avoids self-deadlock.
|
||||
/// </summary>
|
||||
private async Task<BackupOperationResult> CreateBackupCoreAsync(BackupOrigin origin, bool applyRetention, CancellationToken ct)
|
||||
{
|
||||
Stream dump;
|
||||
try
|
||||
{
|
||||
dump = await backupService.CreateDumpAsync(ct);
|
||||
}
|
||||
catch (BackupExecutionException ex)
|
||||
{
|
||||
logger.LogError(ex, "Backup dump failed; no catalog record written.");
|
||||
return new BackupOperationResult(BackupOperationOutcome.Failed, null, ex.Message);
|
||||
}
|
||||
|
||||
await using (dump)
|
||||
{
|
||||
string name = $"backup-{DateTimeOffset.UtcNow:yyyyMMddHHmmssfff}-{Guid.NewGuid():N}.sql";
|
||||
long sizeBytes = dump.Length;
|
||||
|
||||
try
|
||||
{
|
||||
await storage.StoreAsync(name, dump, ct);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(ex, "Failed to store backup dump; no catalog record written.");
|
||||
return new BackupOperationResult(BackupOperationOutcome.Failed, null, ex.Message);
|
||||
}
|
||||
|
||||
BackupRecord record = new()
|
||||
{
|
||||
CreatedBy = AuditConstants.SystemUser,
|
||||
StoragePath = name,
|
||||
SizeBytes = sizeBytes,
|
||||
Origin = origin,
|
||||
};
|
||||
BackupRecord added = await catalog.AddAsync(record, ct);
|
||||
|
||||
if (applyRetention)
|
||||
{
|
||||
await ApplyRetentionAsync(ct);
|
||||
}
|
||||
|
||||
return new BackupOperationResult(BackupOperationOutcome.Completed, BackupRecordResponse.FromEntity(added), null);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyRetentionAsync(CancellationToken ct)
|
||||
{
|
||||
IReadOnlyList<BackupRecord> all = await catalog.ListNewestFirstAsync(ct);
|
||||
IReadOnlyList<BackupFile> files = all
|
||||
.Select(r => new BackupFile(r.StoragePath, new DateTimeOffset(DateTime.SpecifyKind(r.DateCreated, DateTimeKind.Utc))))
|
||||
.ToList();
|
||||
|
||||
IReadOnlyList<BackupFile> toDelete = retentionPolicy.SelectForDeletion(files, options.RetentionCount);
|
||||
if (toDelete.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, BackupRecord> byStoragePath = all.ToDictionary(r => r.StoragePath, StringComparer.Ordinal);
|
||||
IEnumerable<BackupRecord> recordsToDelete = toDelete
|
||||
.Select(stale => byStoragePath.GetValueOrDefault(stale.Name))
|
||||
.OfType<BackupRecord>();
|
||||
|
||||
foreach (BackupRecord record in recordsToDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
await storage.DeleteAsync(record.StoragePath, ct);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(ex, "Failed to delete stale backup file {StoragePath} during retention pruning.", record.StoragePath);
|
||||
}
|
||||
|
||||
await catalog.RemoveAsync(record.Id, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Application.Interfaces.Backup;
|
||||
using Application.Utils.Constants;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Application.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Pure keep-last-N retention policy that retains the newest entries by timestamp with no I/O.
|
||||
/// </summary>
|
||||
public sealed class KeepLastNRetentionPolicy : IBackupRetentionPolicy
|
||||
{
|
||||
public IReadOnlyList<BackupFile> SelectForDeletion(IReadOnlyList<BackupFile> existing, int retainCount)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(existing);
|
||||
|
||||
if (retainCount < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(retainCount), ErrorMessages.Backup.RetentionCountNegative);
|
||||
}
|
||||
|
||||
if (existing.Count <= retainCount)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return existing
|
||||
.OrderByDescending(f => f.Timestamp)
|
||||
.ThenBy(f => f.Name, StringComparer.Ordinal)
|
||||
.Skip(retainCount)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Application.Interfaces.Backup;
|
||||
|
||||
using System;
|
||||
|
||||
namespace Application.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory IMaintenanceModeState singleton, pure process state with no OS or network I/O.
|
||||
/// </summary>
|
||||
public sealed class MaintenanceModeState : IMaintenanceModeState
|
||||
{
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
public string? Reason { get; private set; }
|
||||
|
||||
public DateTimeOffset? EnteredAtUtc { get; private set; }
|
||||
|
||||
public void Enter(string reason)
|
||||
{
|
||||
Reason = reason;
|
||||
EnteredAtUtc = DateTimeOffset.UtcNow;
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public void Exit()
|
||||
{
|
||||
IsActive = false;
|
||||
Reason = null;
|
||||
EnteredAtUtc = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Application.DTOs.Abstract.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the ordering request properties.
|
||||
/// </summary>
|
||||
public interface IOrderRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The property name to sort by.
|
||||
/// </summary>
|
||||
string? OrderBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sort order. Default is Ascending.
|
||||
/// </summary>
|
||||
SortOrder? Order { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Application.DTOs.Abstract.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the pagination request properties.
|
||||
/// </summary>
|
||||
public interface IPaginationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The page number for pagination. Default is 1.
|
||||
/// </summary>
|
||||
int PageNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The page size for pagination. Defaults to 100.
|
||||
/// </summary>
|
||||
int PageSize { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Application.Utils.Constants.Pagination;
|
||||
|
||||
namespace Application.DTOs.Abstract.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request for paginated and filtered data.
|
||||
/// </summary>
|
||||
public class PaginatedFilterRequest : IPaginationRequest, IOrderRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The page number for pagination. Default is 1.
|
||||
/// </summary>
|
||||
public int PageNumber { get; set; } = 1;
|
||||
|
||||
private int _pageSize = PaginationDefaults.DefaultPageSize;
|
||||
|
||||
/// <summary>
|
||||
/// The page size for pagination; values outside 1-100 are silently clamped rather than rejected.
|
||||
/// </summary>
|
||||
public int PageSize
|
||||
{
|
||||
get => _pageSize;
|
||||
set => _pageSize = ClampPageSize(value);
|
||||
}
|
||||
|
||||
private static int ClampPageSize(int requestedPageSize)
|
||||
{
|
||||
if (requestedPageSize < 1)
|
||||
{
|
||||
return PaginationDefaults.DefaultPageSize;
|
||||
}
|
||||
|
||||
return requestedPageSize > PaginationDefaults.MaxPageSize
|
||||
? PaginationDefaults.MaxPageSize
|
||||
: requestedPageSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The property name to sort by.
|
||||
/// </summary>
|
||||
public string? OrderBy { get; set; } = "DateCreated";
|
||||
|
||||
/// <summary>
|
||||
/// The sort order. Default is Ascending.
|
||||
/// </summary>
|
||||
public SortOrder? Order { get; set; } = SortOrder.Ascending;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Abstract.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Enum to specify the sort order.
|
||||
/// </summary>
|
||||
public enum SortOrder
|
||||
{
|
||||
/// <summary>
|
||||
/// Ascending order.
|
||||
/// </summary>
|
||||
[Display(Name = "asc")]
|
||||
Ascending,
|
||||
|
||||
/// <summary>
|
||||
/// Descending order.
|
||||
/// </summary>
|
||||
[Display(Name = "desc")]
|
||||
Descending
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Abstract.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Base response class containing common properties for responses.
|
||||
/// </summary>
|
||||
public class BaseEntityResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier of the entity.
|
||||
/// </summary>
|
||||
public required Guid Id { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Application.DTOs.Abstract.Response;
|
||||
|
||||
/// <summary>
|
||||
/// A page of results together with the metadata needed to fetch adjacent pages.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of item contained in the page.</typeparam>
|
||||
public class PaginatedResponse<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The items in the collection.
|
||||
/// </summary>
|
||||
public IEnumerable<T> Items { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The current page number.
|
||||
/// </summary>
|
||||
public required int Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of items in a page.
|
||||
/// </summary>
|
||||
public required int PageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total number of items in the collection.
|
||||
/// </summary>
|
||||
public required int TotalCount { get; set; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Application.DTOs.Abstract.Request;
|
||||
using Application.Utils.Constants.Pagination;
|
||||
|
||||
using Domain.Enums;
|
||||
|
||||
namespace Application.DTOs.AuditLogs.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Filtering and pagination for the audit-trail listing, defaulting to newest-first.
|
||||
/// </summary>
|
||||
public class AuditLogFilteredRequest : PaginatedFilterRequest
|
||||
{
|
||||
public AuditLogFilteredRequest()
|
||||
{
|
||||
Order = SortOrder.Descending;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional filter by the actor who performed the action.
|
||||
/// </summary>
|
||||
public string? Actor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional filter by the action type.
|
||||
/// </summary>
|
||||
public AuditAction? Action { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.AuditLogs.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Audit-trail entry as returned by the listing endpoint.
|
||||
/// </summary>
|
||||
public class AuditLogResponse : BaseEntityResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The sensitive action that was performed, as the enum name.
|
||||
/// </summary>
|
||||
public required string Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Who performed the action, as an email address or "System".
|
||||
/// </summary>
|
||||
public required string Actor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The kind of entity targeted, when applicable.
|
||||
/// </summary>
|
||||
public string? TargetType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the targeted entity, when applicable.
|
||||
/// </summary>
|
||||
public string? TargetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Null when there is no single named target or the entry predates this field.
|
||||
/// </summary>
|
||||
public string? TargetName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Free-form human-readable context.
|
||||
/// </summary>
|
||||
public string? Detail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the action happened, in UTC.
|
||||
/// </summary>
|
||||
public required DateTime Timestamp { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Auth.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Payload the invited user submits from the activation email link to set their first password.
|
||||
/// </summary>
|
||||
public sealed class ActivateAccountRequest
|
||||
{
|
||||
[Required, EmailAddress]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Activation token received via email, URL-decoded automatically by the framework.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
[Required, MinLength(UserFieldLengths.PasswordMinLength)]
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Auth.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request to create a user by email only; the system emails a magic activation link to set the password.
|
||||
/// </summary>
|
||||
public sealed class InviteUserRequest
|
||||
{
|
||||
[Required(ErrorMessage = "Email is required.")]
|
||||
[EmailAddress(ErrorMessage = "Invalid email format.")]
|
||||
public required string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional contact phone number.
|
||||
/// </summary>
|
||||
[ArgentinePhoneNumber]
|
||||
[MaxLength(UserFieldLengths.PhoneMaxLength, ErrorMessage = "Phone number must not exceed 15 characters.")]
|
||||
public string? Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Target role. Accepted values: ADMIN, OWNER.
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "Role is required.")]
|
||||
public required string Role { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
namespace Application.DTOs.Auth.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request object for user login.
|
||||
/// </summary>
|
||||
public class LogInUserRequest
|
||||
{
|
||||
[Required(ErrorMessage = "The Email field is required.")]
|
||||
public required string Email { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "The Password field is required.")]
|
||||
public required string Password { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Auth.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request to login using a magic-link token.
|
||||
/// </summary>
|
||||
public sealed class MagicLinkLoginRequest
|
||||
{
|
||||
[Required(ErrorMessage = "The Email field is required.")]
|
||||
[EmailAddress(ErrorMessage = "Invalid email format.")]
|
||||
public required string Email { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "The Token field is required.")]
|
||||
public required string Token { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Auth.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request to generate a magic-link login token.
|
||||
/// </summary>
|
||||
public sealed class MagicLinkRequest
|
||||
{
|
||||
[Required(ErrorMessage = "The Email field is required.")]
|
||||
[EmailAddress(ErrorMessage = "Invalid email format.")]
|
||||
public required string Email { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Auth.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Payload for confirming a password reset from the email link.
|
||||
/// </summary>
|
||||
public sealed class PasswordResetConfirmRequest
|
||||
{
|
||||
[Required, EmailAddress]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Token received via email, URL-decoded automatically by the framework.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
[Required, MinLength(UserFieldLengths.PasswordMinLength)]
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Application.DTOs.Auth.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request object for refreshing a JWT token.
|
||||
/// </summary>
|
||||
public class RefreshTokenRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The refresh token to use for generating a new JWT token.
|
||||
/// </summary>
|
||||
public required string RefreshToken { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Auth.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request to register a new user. Password is generated automatically by the system.
|
||||
/// </summary>
|
||||
public sealed class RegisterUserRequest
|
||||
{
|
||||
[Required(ErrorMessage = "Email is required.")]
|
||||
[EmailAddress(ErrorMessage = "Invalid email format.")]
|
||||
public required string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the user; must be unique across all accounts.
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "Username is required.")]
|
||||
[StringLength(UserFieldLengths.UsernameMaxLength, MinimumLength = UserFieldLengths.UsernameMinLength,
|
||||
ErrorMessage = "Username must be between 3 and 50 characters.")]
|
||||
public required string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional contact phone number.
|
||||
/// </summary>
|
||||
[ArgentinePhoneNumber]
|
||||
[MaxLength(UserFieldLengths.PhoneMaxLength, ErrorMessage = "Phone number must not exceed 15 characters.")]
|
||||
public string? Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Target role. Accepted values: ADMIN, OWNER.
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "Role is required.")]
|
||||
public required string Role { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Auth.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Self-service request to receive a password-reset magic link by email.
|
||||
/// </summary>
|
||||
public sealed class RequestPasswordResetRequest
|
||||
{
|
||||
[Required(ErrorMessage = "The Email field is required.")]
|
||||
[EmailAddress(ErrorMessage = "Invalid email format.")]
|
||||
public required string Email { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Auth.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Confirmation payload returned after inviting a user by email; the account has no password until activation.
|
||||
/// </summary>
|
||||
public sealed class InviteUserResponse(Guid userId, string email, string role)
|
||||
{
|
||||
public Guid UserId { get; init; } = userId;
|
||||
public string Email { get; init; } = email;
|
||||
public string Role { get; init; } = role;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Application.DTOs.Auth.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Response for magic-link request flow.
|
||||
/// </summary>
|
||||
public sealed class MagicLinkResponse(string message, string? magicLink = null)
|
||||
{
|
||||
public string Message { get; init; } = message;
|
||||
public string? MagicLink { get; init; } = magicLink;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Auth.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Confirmation payload returned after successfully registering a new user.
|
||||
/// </summary>
|
||||
public sealed class RegisterUserResponse(Guid userId, string email, string username, string role, string? phone)
|
||||
{
|
||||
public Guid UserId { get; init; } = userId;
|
||||
public string Email { get; init; } = email;
|
||||
public string Username { get; init; } = username;
|
||||
public string Role { get; init; } = role;
|
||||
public string? PhoneNumber { get; init; } = phone;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Auth.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response containing access token, expiration time, and refresh token.
|
||||
/// </summary>
|
||||
public sealed class TokenResponse(string accessToken, TimeSpan expiresIn, string? refreshToken)
|
||||
{
|
||||
/// <summary>
|
||||
/// The access token used for authorization.
|
||||
/// </summary>
|
||||
public string AccessToken { get; init; } = accessToken;
|
||||
|
||||
public TimeSpan ExpiresIn { get; init; } = expiresIn;
|
||||
|
||||
/// <summary>
|
||||
/// The refresh token used to obtain a new access token.
|
||||
/// </summary>
|
||||
public string? RefreshToken { get; init; } = refreshToken;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Backup.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Response projection of a catalogued BackupRecord, with Origin serialized as its enum name.
|
||||
/// </summary>
|
||||
public sealed record BackupRecordResponse(
|
||||
Guid Id, DateTime CreatedAt, long SizeBytes, string Origin, string StoragePath)
|
||||
{
|
||||
public static BackupRecordResponse FromEntity(BackupRecord record)
|
||||
{
|
||||
return new BackupRecordResponse(
|
||||
record.Id, record.DateCreated, record.SizeBytes, record.Origin.ToString(), record.StoragePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Backup.Response;
|
||||
|
||||
/// <summary>
|
||||
/// The current maintenance window; Reason and EnteredAtUtc are null when IsActive is false.
|
||||
/// </summary>
|
||||
public sealed record MaintenanceStatusResponse(bool IsActive, string? Reason, DateTimeOffset? EnteredAtUtc);
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.BlogPosts.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to create a new blog post, including file upload for the photo.
|
||||
/// </summary>
|
||||
public class CreateBlogPostRequest
|
||||
{
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public required string Author { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string Title { get; set; }
|
||||
|
||||
public IFormFile? PhotoFile { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string MarkdownText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the post is published or a draft; defaults to true so existing create flows are unaffected.
|
||||
/// </summary>
|
||||
public bool IsPublished { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Application.DTOs.Abstract.Request;
|
||||
|
||||
namespace Application.DTOs.BlogPosts.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to get filtered blog posts.
|
||||
/// </summary>
|
||||
public class GetBlogPostsFilteredRequest : PaginatedFilterRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Overrides the base PaginatedFilterRequest default to sort newest-created-first.
|
||||
/// </summary>
|
||||
public GetBlogPostsFilteredRequest()
|
||||
{
|
||||
// String literal matches EntityBase.DateCreated; QueryableExtensions.SortBy resolves this name against the entity by reflection.
|
||||
OrderBy = "DateCreated";
|
||||
Order = SortOrder.Descending;
|
||||
}
|
||||
|
||||
public string? Author { get; set; }
|
||||
|
||||
public string? Title { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.BlogPosts.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to update the photo for a blog post.
|
||||
/// </summary>
|
||||
public class UpdateBlogPostPhotoRequest
|
||||
{
|
||||
[Required]
|
||||
public required IFormFile PhotoFile { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Application.DTOs.BlogPosts.Request;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to update a blog post.
|
||||
/// </summary>
|
||||
public class UpdateBlogPostRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Null leaves the current title unchanged.
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Null leaves the current Markdown content unchanged.
|
||||
/// </summary>
|
||||
public string? MarkdownText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Null leaves the current author unchanged.
|
||||
/// </summary>
|
||||
public string? Author { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The updated publication state; null leaves it unchanged, true publishes, false reverts to a draft.
|
||||
/// </summary>
|
||||
public bool? IsPublished { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.BlogPosts.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response data for a blog post.
|
||||
/// </summary>
|
||||
public class BlogPostResponse : BaseEntityResponse
|
||||
{
|
||||
public required string Author { get; set; }
|
||||
|
||||
public required string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The unique, URL-friendly identifier used in public blog post links.
|
||||
/// </summary>
|
||||
public required string Slug { get; set; }
|
||||
|
||||
public int Views { get; set; }
|
||||
|
||||
public string? PhotoUrl { get; set; }
|
||||
|
||||
public required string MarkdownText { get; set; }
|
||||
|
||||
public required DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the post is published and visible publicly, or is a draft.
|
||||
/// </summary>
|
||||
public bool IsPublished { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Champions.Response;
|
||||
|
||||
/// <summary>
|
||||
/// One row of the champions history for a finished division, with context to render a table across seasons.
|
||||
/// </summary>
|
||||
public class ChampionHistoryResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The id of the tournament the champion was crowned in.
|
||||
/// </summary>
|
||||
public required Guid TournamentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the tournament.
|
||||
/// </summary>
|
||||
public required string TournamentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The season name, or null when the tournament is not grouped under any season.
|
||||
/// </summary>
|
||||
public string? SeasonName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The season's calendar year, or null when there is no season or year set.
|
||||
/// </summary>
|
||||
public int? SeasonYear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The competitive category of the division, Masculine or Feminine.
|
||||
/// </summary>
|
||||
public required string Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the division that was won.
|
||||
/// </summary>
|
||||
public required string DivisionName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the sub-cup that was won, or null when the division crowns a single champion.
|
||||
/// </summary>
|
||||
public string? CupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The team that won this cup, in first place.
|
||||
/// </summary>
|
||||
public required PodiumTeamResponse ChampionTeam { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Champions.Response;
|
||||
|
||||
/// <summary>
|
||||
/// The final podium of a single competition; any place not yet decided is null rather than guessed.
|
||||
/// </summary>
|
||||
public class PodiumResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The id of the division this podium belongs to.
|
||||
/// </summary>
|
||||
public required Guid DivisionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the division this podium belongs to.
|
||||
/// </summary>
|
||||
public required string DivisionName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True when the division's champion is decided by a playoff Final rather than by group standings.
|
||||
/// </summary>
|
||||
public bool HasPlayoff { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The champion in first place, or null when not yet decided.
|
||||
/// </summary>
|
||||
public PodiumTeamResponse? First { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The runner-up in second place, or null when not yet decided.
|
||||
/// </summary>
|
||||
public PodiumTeamResponse? Second { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The team in third place, or null when not yet decided.
|
||||
/// </summary>
|
||||
public PodiumTeamResponse? Third { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Champions.Response;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight reference to a team occupying a podium place, carrying only what a team badge needs.
|
||||
/// </summary>
|
||||
public class PodiumTeamResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier of the team.
|
||||
/// </summary>
|
||||
public required Guid TeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The display name of the team.
|
||||
/// </summary>
|
||||
public required string TeamName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL of the team's logo.
|
||||
/// </summary>
|
||||
public required string LogoUrl { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Club.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Links a club as a squad of the given parent institution club.
|
||||
/// </summary>
|
||||
public class LinkClubParentRequest
|
||||
{
|
||||
public required Guid ParentClubId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Application.DTOs.Club.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Renames a club. The club's slug (used in its public URL) never changes, only its display name.
|
||||
/// </summary>
|
||||
public class RenameClubRequest
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Application.DTOs.Club.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Outcome of the idempotent club backfill; a second run reports zeros.
|
||||
/// </summary>
|
||||
public class ClubBackfillResult
|
||||
{
|
||||
public required int ClubsCreated { get; set; }
|
||||
public required int TeamsLinked { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Application.DTOs.Club.Response;
|
||||
|
||||
/// <summary>
|
||||
/// A club's stable identity plus every per-season team that belongs to it.
|
||||
/// </summary>
|
||||
public class ClubHistoryResponse
|
||||
{
|
||||
public required Guid Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Slug { get; set; }
|
||||
public string? LogoUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The per-season teams that make up this club's history.
|
||||
/// </summary>
|
||||
public required List<ClubTeamSeasonResponse> Teams { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The parent institution this club is a squad of, or null when this club has no parent linked.
|
||||
/// </summary>
|
||||
public ClubSummaryResponse? ParentClub { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Other squads linked to this club as their parent institution, empty when this club has none.
|
||||
/// </summary>
|
||||
public required List<ClubSummaryResponse> ChildClubs { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Club.Response;
|
||||
|
||||
/// <summary>
|
||||
/// One season a club's team participated in.
|
||||
/// </summary>
|
||||
public class ClubSeasonResponse
|
||||
{
|
||||
public required Guid TournamentId { get; set; }
|
||||
public string? TournamentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sort key only, not displayed; defaults to the minimum date so unresolved tournaments sort last.
|
||||
/// </summary>
|
||||
public DateTime StartDate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Club.Response;
|
||||
|
||||
/// <summary>
|
||||
/// A minimal club identity, used for pickers and for referencing a related club (parent/child) without pulling in its full season history.
|
||||
/// </summary>
|
||||
public class ClubSummaryResponse
|
||||
{
|
||||
public required Guid Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Slug { get; set; }
|
||||
public string? LogoUrl { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Application.DTOs.Club.Response;
|
||||
|
||||
/// <summary>
|
||||
/// One per-season team row belonging to a club, with the seasons it participated in.
|
||||
/// </summary>
|
||||
public class ClubTeamSeasonResponse
|
||||
{
|
||||
public required Guid TeamId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Slug { get; set; }
|
||||
public required string ThreeLetterCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tournaments this team was registered in, sourced from its registration records.
|
||||
/// </summary>
|
||||
public required List<ClubSeasonResponse> Seasons { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Application.DTOs.DataMaintenance.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Row counts created by the sample-data seed, for the admin UI's success summary.
|
||||
/// </summary>
|
||||
public sealed record DataSeedResult(
|
||||
int Tournaments,
|
||||
int Divisions,
|
||||
int Teams,
|
||||
int Players,
|
||||
int Matches,
|
||||
int PlayerSanctions,
|
||||
int BlogPosts
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Application.DTOs.DataMaintenance.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Row counts removed by the sample-data wipe, for the admin UI's success summary.
|
||||
/// </summary>
|
||||
public sealed record DataWipeResult(
|
||||
int Tournaments,
|
||||
int Divisions,
|
||||
int Teams,
|
||||
int Players,
|
||||
int Matches,
|
||||
int MatchSeries,
|
||||
int PlayerSanctions,
|
||||
int PlayerStatistics,
|
||||
int Scorers,
|
||||
int StageTeamMatches,
|
||||
int PlayerTeamRegistrations,
|
||||
int Stages,
|
||||
int Venues,
|
||||
int BlogPosts
|
||||
);
|
||||
@@ -0,0 +1,54 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
namespace Application.DTOs.Divisions.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to create a division.
|
||||
/// </summary>
|
||||
public class CreateDivisionRequest
|
||||
{
|
||||
[Required(ErrorMessage = "The Name field is required.")]
|
||||
[MaxLength(DivisionFieldLengths.NameMaxLength)]
|
||||
public required string Name { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "The TournamentId field is required.")]
|
||||
public required Guid TournamentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Marks this division as a cross-division cup that intentionally draws teams from every division.
|
||||
/// </summary>
|
||||
public bool IsCrossDivisionCup { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Points awarded for a win in this division's standings; defaults to 2.
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue, ErrorMessage = "PointsForWin cannot be negative.")]
|
||||
public int PointsForWin { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Points awarded for a loss in this division's standings; defaults to 1.
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue, ErrorMessage = "PointsForLoss cannot be negative.")]
|
||||
public int PointsForLoss { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// How many teams qualify from each internal group of a multi-group cross-division cup; defaults to 1.
|
||||
/// </summary>
|
||||
[Range(1, int.MaxValue, ErrorMessage = "QualifiersPerGroup must be at least 1.")]
|
||||
public int QualifiersPerGroup { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Competitive category of the division, which must match the parent tournament's category.
|
||||
/// </summary>
|
||||
public TournamentCategory Category { get; set; } = TournamentCategory.Masculine;
|
||||
|
||||
/// <summary>
|
||||
/// Optional position-range to playoff-destination mapping the wizard sends to seed multiple cups.
|
||||
/// </summary>
|
||||
public List<PlayoffMappingRequest>? PlayoffMappings { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Application.DTOs.Divisions.Request;
|
||||
|
||||
public class EnrollTeamsRequest
|
||||
{
|
||||
public List<Guid> TeamIds { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Application.DTOs.Abstract.Request;
|
||||
|
||||
using System;
|
||||
namespace Application.DTOs.Divisions.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to get filtered divisions.
|
||||
/// </summary>
|
||||
public class GetDivisionsFilteredRequest : PaginatedFilterRequest
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
public bool? IsFinished { get; set; }
|
||||
|
||||
public Guid? TournamentId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Divisions.Request;
|
||||
|
||||
/// <summary>
|
||||
/// One position-range to playoff-destination entry the wizard sends for a division; ranges must not overlap.
|
||||
/// </summary>
|
||||
public class PlayoffMappingRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// First standings position in the range, 1-based and inclusive.
|
||||
/// </summary>
|
||||
[Range(1, int.MaxValue, ErrorMessage = "FromPosition must be 1 or greater.")]
|
||||
public required int FromPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last standings position in the range, 1-based and inclusive.
|
||||
/// </summary>
|
||||
[Range(1, int.MaxValue, ErrorMessage = "ToPosition must be 1 or greater.")]
|
||||
public required int ToPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The playoff cup the teams in this range qualify for, matching an elimination stage's BracketName.
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "The Destination field is required.")]
|
||||
public required string Destination { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Divisions.Request;
|
||||
|
||||
public class ReassignTeamToSubGroupRequest
|
||||
{
|
||||
public required Guid TeamId { get; set; }
|
||||
|
||||
public required Guid FromStageId { get; set; }
|
||||
|
||||
public required Guid ToStageId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Divisions.Request;
|
||||
|
||||
public class RebuildSubGroupsRequest
|
||||
{
|
||||
[Range(1, int.MaxValue, ErrorMessage = "SubGroupCount must be at least 1.")]
|
||||
public required int SubGroupCount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Application.DTOs.Divisions.Request;
|
||||
|
||||
public class UnenrollTeamsRequest
|
||||
{
|
||||
public List<Guid> TeamIds { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
namespace Application.DTOs.Divisions.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to create a division.
|
||||
/// </summary>
|
||||
public class UpdateDivisionRequest
|
||||
{
|
||||
[Required(ErrorMessage = "The Name field is required.")]
|
||||
[MaxLength(DivisionFieldLengths.NameMaxLength)]
|
||||
public required string Name { get; set; }
|
||||
|
||||
public required bool IsFinished { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When provided, moves the division and everything under it to this tournament; null keeps it in place.
|
||||
/// </summary>
|
||||
public Guid? TournamentId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.Match.Response;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace Application.DTOs.Divisions.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response for a division, inheriting from the base response.
|
||||
/// </summary>
|
||||
public class DetailedDivisionResponse : BaseEntityResponse
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
|
||||
public required bool IsFinished { get; set; }
|
||||
|
||||
public required IEnumerable<PositionResponse> Positions { get; set; }
|
||||
|
||||
public required Guid TournamentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The matches grouped by week in the division.
|
||||
/// </summary>
|
||||
public required IDictionary<int, IEnumerable<MinimalMatchResponse>> MatchesByWeek { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace Application.DTOs.Divisions.Response;
|
||||
|
||||
/// <summary>
|
||||
/// The response structure for a division, including its matches and positions.
|
||||
/// </summary>
|
||||
public class DivisionResponse
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The unique, URL-friendly identifier used in public division links.
|
||||
/// </summary>
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
|
||||
public bool IsFinished { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For a multi-group cup this is the pooled union across every group; use GroupStandings for per-group tables.
|
||||
/// </summary>
|
||||
public List<PositionResponse>? Positions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// One standings table per Group stage, null or empty when the division has no Group stage yet.
|
||||
/// </summary>
|
||||
public List<GroupStandingsResponse>? GroupStandings { get; set; }
|
||||
|
||||
public Guid TournamentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The parent tournament's slug, when loaded; null otherwise, for building a clean tournament link.
|
||||
/// </summary>
|
||||
public string? TournamentSlug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this division is a cross-division cup that intentionally draws teams from every division.
|
||||
/// </summary>
|
||||
public bool IsCrossDivisionCup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Competitive category of the division, always matching the parent tournament's category.
|
||||
/// </summary>
|
||||
public TournamentCategory Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Points awarded for a win in this division's standings.
|
||||
/// </summary>
|
||||
public int PointsForWin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Points awarded for a loss in this division's standings.
|
||||
/// </summary>
|
||||
public int PointsForLoss { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How many teams qualify from each internal group of a multi-group cup; defaults to 1.
|
||||
/// </summary>
|
||||
public int QualifiersPerGroup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The division's position-range to playoff-destination mapping.
|
||||
/// </summary>
|
||||
public List<PlayoffMappingResponse>? PlayoffMappings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The standings-position ranges that qualify to a playoff cup, ordered top-down; empty when none exist.
|
||||
/// </summary>
|
||||
public List<QualificationRangeResponse>? QualificationRanges { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Application.DTOs.Divisions.Response;
|
||||
|
||||
/// <summary>
|
||||
/// The standings of a single Group stage; a multi-group division sends one entry per internal group.
|
||||
/// </summary>
|
||||
public class GroupStandingsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The id of the Group stage these standings belong to.
|
||||
/// </summary>
|
||||
public Guid StageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Group stage's name, used as the table label.
|
||||
/// </summary>
|
||||
public string StageName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The ordered standings for the teams in this group.
|
||||
/// </summary>
|
||||
public List<PositionResponse> Positions { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
namespace Application.DTOs.Divisions.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a minimal response for a tournament.
|
||||
/// </summary>
|
||||
public class MinimalDivisionResponse : BaseEntityResponse
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
|
||||
public required bool IsFinished { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Divisions.Response;
|
||||
|
||||
/// <summary>
|
||||
/// One position-range to playoff-destination entry of a division.
|
||||
/// </summary>
|
||||
public class PlayoffMappingResponse
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public int FromPosition { get; set; }
|
||||
public int ToPosition { get; set; }
|
||||
public string Destination { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
|
||||
using Application.DTOs.PointDeductions.Response;
|
||||
|
||||
namespace Application.DTOs.Divisions.Response;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Represents the position of a team in a division, including various match statistics.
|
||||
/// </summary>
|
||||
public class PositionResponse
|
||||
{
|
||||
public required Guid TeamId { get; set; }
|
||||
|
||||
public required string TeamName { get; set; }
|
||||
|
||||
public required string LogoUrl { get; set; }
|
||||
|
||||
public required int MatchesPlayed { get; set; }
|
||||
|
||||
public required int Wins { get; set; }
|
||||
|
||||
public required int Losses { get; set; }
|
||||
|
||||
public required int PointsFor { get; set; }
|
||||
|
||||
public required int PointsAgainst { get; set; }
|
||||
|
||||
public required int PointsDifference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total points accumulated by the team, with any disciplinary deduction already subtracted.
|
||||
/// </summary>
|
||||
public required int Points { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The disciplinary point deduction applied to this team, or null when there is none.
|
||||
/// </summary>
|
||||
public AppliedPointDeductionResponse? PointDeduction { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace Application.DTOs.Divisions.Response;
|
||||
|
||||
/// <summary>
|
||||
/// One standings-position range that qualifies to a playoff cup, derived from the division's mapping set.
|
||||
/// </summary>
|
||||
public class QualificationRangeResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// First standings position in the range, 1-based and inclusive.
|
||||
/// </summary>
|
||||
public int FromPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last standings position in the range, 1-based and inclusive.
|
||||
/// </summary>
|
||||
public int ToPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cup the teams in this range qualify for.
|
||||
/// </summary>
|
||||
public string CupName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The cup's rank, top-down, where 0 is the top cup; drives the color the frontend paints each range.
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
|
||||
namespace Application.DTOs.Match.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to create a match.
|
||||
/// </summary>
|
||||
public class CreateMatchRequest
|
||||
{
|
||||
[Required(ErrorMessage = "The MatchDate field is required.")]
|
||||
public required DateTime MatchDate { get; set; }
|
||||
|
||||
[AllowedValues(MatchType.Regular, MatchType.Playoff)]
|
||||
public MatchType? Type { get; set; } = MatchType.Regular;
|
||||
|
||||
[Required(ErrorMessage = "The HomeTeamId field is required.")]
|
||||
public required Guid HomeTeamId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "The VisitorTeamId field is required.")]
|
||||
public required Guid VisitorTeamId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "The DivisionId field is required.")]
|
||||
public required Guid StageId { get; set; }
|
||||
|
||||
public Guid? VenueId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Application.DTOs.Abstract.Request;
|
||||
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Match.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to filter and paginate matches.
|
||||
/// </summary>
|
||||
public class GetMatchesFilteredRequest : PaginatedFilterRequest
|
||||
{
|
||||
public string? HomeTeamName { get; set; }
|
||||
|
||||
public string? VisitorTeamName { get; set; }
|
||||
|
||||
public Guid? StageId { get; set; }
|
||||
|
||||
public Guid? DivisionId { get; set; }
|
||||
|
||||
public Guid? TournamentId { get; set; }
|
||||
|
||||
public MatchType? Type { get; set; }
|
||||
|
||||
public bool? IsFinished { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Match.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request to mark a match as a walkover, awarding the present team the regulation default result.
|
||||
/// </summary>
|
||||
public class LoadWalkOverRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The walkover winner; must be one of the match's two teams.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid PresentTeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional override for the present team's awarded score; the regulation default is used when omitted.
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue, ErrorMessage = "The PresentTeamScore must be a non-negative number.")]
|
||||
public int? PresentTeamScore { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Match.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request to suspend a match and optionally reschedule it, never changing its round.
|
||||
/// </summary>
|
||||
public class SuspendMatchRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional new date and time for the match; null suspends it in place without rescheduling.
|
||||
/// </summary>
|
||||
public DateTime? MatchDate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Match.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request to reschedule a match; edits only the calendar date and venue.
|
||||
/// </summary>
|
||||
public class UpdateMatchRequest
|
||||
{
|
||||
public DateTime? MatchDate { get; set; }
|
||||
|
||||
public Guid? VenueId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Match.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to update the score of a match.
|
||||
/// </summary>
|
||||
public class UpdateMatchScoreRequest
|
||||
{
|
||||
[Required]
|
||||
[Range(0, int.MaxValue, ErrorMessage = "The HomeScore must be a non-negative number.")]
|
||||
public required int HomeScore { get; set; }
|
||||
|
||||
[Required]
|
||||
[Range(0, int.MaxValue, ErrorMessage = "The VisitorScore must be a non-negative number.")]
|
||||
public required int VisitorScore { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
using Application.DTOs.Team.Response;
|
||||
using Application.DTOs.Venue.Response;
|
||||
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Match.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response data for a match.
|
||||
/// </summary>
|
||||
public class DetailedMatchResponse : BaseEntityResponse
|
||||
{
|
||||
public required DateTime MatchDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The 1-based matchday this match belongs to; null for stages with no round-robin matchday.
|
||||
/// </summary>
|
||||
public int? Round { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the match, regular or playoff.
|
||||
/// </summary>
|
||||
public required string MatchType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The unique, URL-friendly identifier used in public match links.
|
||||
/// </summary>
|
||||
public required string Slug { get; set; }
|
||||
|
||||
public TeamDetailedMatchResponse? HomeTeam { get; set; }
|
||||
|
||||
public TeamDetailedMatchResponse? VisitorTeam { get; set; }
|
||||
|
||||
public VenueResponse? Venue { get; set; }
|
||||
|
||||
public required bool IsFinished { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The result lifecycle state: Scheduled, Played, Suspended, or WalkOver.
|
||||
/// </summary>
|
||||
public string? Status { get; set; }
|
||||
|
||||
public bool WentToOvertime { get; set; }
|
||||
|
||||
public string? WinningTeamName { get; set; }
|
||||
|
||||
public Guid? WinningTeamId { get; set; }
|
||||
|
||||
public Guid? StageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The id of the tournament this match belongs to, letting the public page link back to it directly.
|
||||
/// </summary>
|
||||
public Guid? TournamentId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Match.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the minimal response data for a match, tailored for divisions.
|
||||
/// </summary>
|
||||
public class MinimalMatchResponse : BaseEntityResponse
|
||||
{
|
||||
public required DateTime MatchDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The 1-based matchday this match belongs to; null for stages with no round-robin matchday.
|
||||
/// </summary>
|
||||
public int? Round { get; set; }
|
||||
|
||||
public required string HomeTeamName { get; set; }
|
||||
|
||||
public required string VisitorTeamName { get; set; }
|
||||
|
||||
public int? HomeScore { get; set; }
|
||||
|
||||
public int? VisitorScore { get; set; }
|
||||
|
||||
public string? WinningTeamName { get; set; }
|
||||
|
||||
public required bool IsFinished { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The result lifecycle state: Scheduled, Played, Suspended, or WalkOver.
|
||||
/// </summary>
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the match, regular or playoff.
|
||||
/// </summary>
|
||||
public required string MatchType { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Application.DTOs.Match.Response;
|
||||
|
||||
/// <summary>
|
||||
/// A single matchday and its matches, letting the frontend group the fixture by round instead of date.
|
||||
/// </summary>
|
||||
public class RoundMatchesResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The 1-based round number; null groups matches with no round-robin matchday.
|
||||
/// </summary>
|
||||
public int? Round { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The matches played in this round, in a stable order.
|
||||
/// </summary>
|
||||
public List<DetailedMatchResponse> Matches { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.MatchSeries.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Payload for scheduling the next game of an existing series; GameNumber and teams are derived server-side.
|
||||
/// </summary>
|
||||
public class AddGameToSeriesRequest
|
||||
{
|
||||
[Required(ErrorMessage = "Match date field is required.")]
|
||||
public required DateTime MatchDate { get; set; }
|
||||
|
||||
public Guid? VenueId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.MatchSeries.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Payload for creating a new best-of-N playoff series between two teams at one bracket round.
|
||||
/// </summary>
|
||||
public class CreateMatchSeriesRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The stage this series belongs to; its BestOf value is copied onto the series at creation time.
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "Stage ID field is required.")]
|
||||
public required Guid StageId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Home team ID field is required.")]
|
||||
public required Guid HomeTeamId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Visitor team ID field is required.")]
|
||||
public required Guid VisitorTeamId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Application.DTOs.Abstract.Request;
|
||||
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.MatchSeries.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to filter and paginate playoff series.
|
||||
/// </summary>
|
||||
public class GetMatchSeriesFilteredRequest : PaginatedFilterRequest
|
||||
{
|
||||
public Guid? StageId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Application.DTOs.MatchSeries.Response;
|
||||
|
||||
/// <summary>
|
||||
/// A best-of-N playoff series between two teams at one bracket round, including its individual games.
|
||||
/// </summary>
|
||||
public class MatchSeriesResponse : BaseEntityResponse
|
||||
{
|
||||
public required Guid StageId { get; set; }
|
||||
|
||||
public required Guid HomeTeamId { get; set; }
|
||||
|
||||
public required string HomeTeamName { get; set; }
|
||||
|
||||
public required Guid VisitorTeamId { get; set; }
|
||||
|
||||
public required string VisitorTeamName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of games in this series, one of 1, 3, 5, or 7.
|
||||
/// </summary>
|
||||
public required int BestOf { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The id of the winning team, set once one team has won the majority of the series' games.
|
||||
/// </summary>
|
||||
public Guid? WinningTeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the winning team, if the series has been decided.
|
||||
/// </summary>
|
||||
public string? WinningTeamName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The individual games played so far in this series.
|
||||
/// </summary>
|
||||
public List<SeriesGameResponse> Games { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Application.DTOs.Match.Response;
|
||||
|
||||
namespace Application.DTOs.MatchSeries.Response;
|
||||
|
||||
/// <summary>
|
||||
/// A single game within a best-of-N series, with its position in the series alongside the match details.
|
||||
/// </summary>
|
||||
public class SeriesGameResponse : MinimalMatchResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The game's 1-based position within the series.
|
||||
/// </summary>
|
||||
public required int GameNumber { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.MedicalRecord.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Owner or admin request to approve or reject a player's medical record for a team and tournament.
|
||||
/// </summary>
|
||||
public class ReviewMedicalRecordRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The player whose medical record is being reviewed.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid PlayerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The team the player is registered to for the season.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid TeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tournament the record applies to.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid TournamentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True to approve and make the player habilitado; false to reject.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required bool Approve { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional reason, typically recorded when rejecting.
|
||||
/// </summary>
|
||||
[MaxLength(500)]
|
||||
public string? Reason { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.MedicalRecord.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Multipart request to upload a player's medical-record PDF for a specific team and tournament.
|
||||
/// </summary>
|
||||
public class UploadMedicalRecordRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The player the medical record belongs to.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid PlayerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The team the player is registered to for the season.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid TeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tournament the record applies to.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid TournamentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The medical-record PDF to upload.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required IFormFile File { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Domain.Entities.Models;
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.MedicalRecord.Response;
|
||||
|
||||
/// <summary>
|
||||
/// The medical-record eligibility state of a player's season registration, flagging not-habilitado players.
|
||||
/// </summary>
|
||||
public class MedicalRecordResponse
|
||||
{
|
||||
public required Guid PlayerId { get; set; }
|
||||
public required Guid TeamId { get; set; }
|
||||
public required Guid TournamentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The medical-record status: Pending, Approved, or Rejected.
|
||||
/// </summary>
|
||||
public required MedicalRecordStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True only when the record is Approved.
|
||||
/// </summary>
|
||||
public required bool IsHabilitado { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Storage reference of the uploaded file, or null if none yet.
|
||||
/// </summary>
|
||||
public string? FileUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Original uploaded file name, or null if none yet.
|
||||
/// </summary>
|
||||
public string? FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason recorded on rejection, if any.
|
||||
/// </summary>
|
||||
public string? ReviewReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the record was last approved or rejected, if ever.
|
||||
/// </summary>
|
||||
public DateTime? ReviewedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Projects a PlayerTeamRegistration into its medical-record view.
|
||||
/// </summary>
|
||||
public static MedicalRecordResponse FromRegistration(PlayerTeamRegistration registration)
|
||||
{
|
||||
return new MedicalRecordResponse
|
||||
{
|
||||
PlayerId = registration.PlayerId,
|
||||
TeamId = registration.TeamId,
|
||||
TournamentId = registration.TournamentId,
|
||||
Status = registration.MedicalRecordStatus,
|
||||
IsHabilitado = registration.IsHabilitado,
|
||||
FileUrl = registration.MedicalRecordFileUrl,
|
||||
FileName = registration.MedicalRecordFileName,
|
||||
ReviewReason = registration.MedicalRecordReviewReason,
|
||||
ReviewedAt = registration.MedicalRecordReviewedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
namespace Application.DTOs.Player.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to create a Player.
|
||||
/// </summary>
|
||||
public class CreatePlayerRequest
|
||||
{
|
||||
[Required(ErrorMessage = "The First Name field is required.")]
|
||||
[MaxLength(PlayerFieldLengths.NameMaxLength, ErrorMessage = "The Name field must not exceed 70 characters.")]
|
||||
public required string FirstName { get; set; }
|
||||
|
||||
[MaxLength(PlayerFieldLengths.NameMaxLength, ErrorMessage = "The Second Name field must not exceed 70 characters.")]
|
||||
public string? SecondName { get; set; } = null;
|
||||
|
||||
[Required(ErrorMessage = "The LastName field is required.")]
|
||||
[MaxLength(PlayerFieldLengths.NameMaxLength, ErrorMessage = "The LastName field must not exceed 70 characters.")]
|
||||
public required string LastName { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "The DocumentNumber field is required.")]
|
||||
[MaxLength(PlayerFieldLengths.DocumentNumberMaxLength, ErrorMessage = "The DocumentNumber field must not exceed 15 characters.")]
|
||||
[RegularExpression(ValidationPatterns.DocumentNumber, ErrorMessage = ValidationPatterns.DocumentNumberError)]
|
||||
public required string DocumentNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The birthdate of the player. Must be at least 15 years ago.
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "The BirthDate field is required.")]
|
||||
[MinimumAge(15)]
|
||||
public required DateTime BirthDate { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "The PhoneNumber field is required.")]
|
||||
[MaxLength(PlayerFieldLengths.PhoneNumberMaxLength, ErrorMessage = "The PhoneNumber field must not exceed 15 characters.")]
|
||||
[ArgentinePhoneNumber]
|
||||
public required string PhoneNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The player's health insurance provider or social work plan.
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "The SocialSecurity field is required.")]
|
||||
[MaxLength(PlayerFieldLengths.SocialSecurityMaxLength, ErrorMessage = "The SocialSecurity field must not exceed 100 characters.")]
|
||||
public required string SocialSecurity { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "The TeamId field is required.")]
|
||||
public required Guid TeamId { get; set; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
namespace Application.DTOs.Player.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to get filtered players for admin access.
|
||||
/// </summary>
|
||||
public class GetPlayersFilteredRequest : PlayerFilterRequestBase
|
||||
{
|
||||
public string? DocumentNumber { get; set; }
|
||||
|
||||
public DateTime? BirthDate { get; set; }
|
||||
|
||||
public string? PhoneNumber { get; set; }
|
||||
|
||||
public string? SocialSecurity { get; set; }
|
||||
|
||||
public bool? IsFederated { get; set; }
|
||||
|
||||
public string? Club { get; set; }
|
||||
|
||||
public string? Category { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Application.DTOs.Abstract.Request;
|
||||
|
||||
using System;
|
||||
namespace Application.DTOs.Player.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for filtering players with common properties.
|
||||
/// </summary>
|
||||
public class PlayerFilterRequestBase : PaginatedFilterRequest
|
||||
{
|
||||
public string? Names { get; set; }
|
||||
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public Guid? TeamId { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.Player.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request to register a player onto a team's roster for a tournament season, optionally with a dorsal.
|
||||
/// </summary>
|
||||
public class RegisterPlayerToTeamRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The team to register the player onto.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid TeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tournament the registration belongs to.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid TournamentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The player's jersey number for this team and season, from 0 to 99 inclusive, or null.
|
||||
/// </summary>
|
||||
[Range(0, 99, ErrorMessage = "El dorsal debe ser un número entre 0 y 99.")]
|
||||
public int? JerseyNumber { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
namespace Application.DTOs.Player.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to update a Player.
|
||||
/// </summary>
|
||||
public class UpdatePlayerRequest
|
||||
{
|
||||
[Required(ErrorMessage = "The First Name field is required.")]
|
||||
[MaxLength(PlayerFieldLengths.NameMaxLength, ErrorMessage = "The Name field must not exceed 70 characters.")]
|
||||
public required string FirstName { get; set; }
|
||||
|
||||
[MaxLength(PlayerFieldLengths.NameMaxLength, ErrorMessage = "The Second Name field must not exceed 70 characters.")]
|
||||
public string? SecondName { get; set; } = null;
|
||||
|
||||
[MaxLength(PlayerFieldLengths.NameMaxLength, ErrorMessage = "The LastName field must not exceed 70 characters.")]
|
||||
public string? LastName { get; set; }
|
||||
|
||||
[MaxLength(PlayerFieldLengths.DocumentNumberMaxLength, ErrorMessage = "The DocumentNumber field must not exceed 15 characters.")]
|
||||
[RegularExpression(ValidationPatterns.DocumentNumber, ErrorMessage = ValidationPatterns.DocumentNumberError)]
|
||||
public string? DocumentNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The birthdate of the player. Must be at least 15 years ago.
|
||||
/// </summary>
|
||||
[MinimumAge(15)]
|
||||
public DateTime? BirthDate { get; set; }
|
||||
|
||||
[MaxLength(PlayerFieldLengths.PhoneNumberMaxLength, ErrorMessage = "The PhoneNumber field must not exceed 15 characters.")]
|
||||
[ArgentinePhoneNumber]
|
||||
public string? PhoneNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The player's health insurance provider or social work plan.
|
||||
/// </summary>
|
||||
[MaxLength(PlayerFieldLengths.SocialSecurityMaxLength, ErrorMessage = "The SocialSecurity field must not exceed 100 characters.")]
|
||||
public string? SocialSecurity { get; set; }
|
||||
|
||||
public Guid? TeamId { get; set; }
|
||||
|
||||
public bool? IsFederated { get; set; }
|
||||
|
||||
[MaxLength(100)]
|
||||
public string? Club { get; set; }
|
||||
|
||||
[MaxLength(50)]
|
||||
public string? Category { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
namespace Application.DTOs.Player.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Detailed response for an admin to view player information, extending PublicPlayerResponse with sensitive fields.
|
||||
/// </summary>
|
||||
public class AdminPlayerResponse : PublicPlayerResponse
|
||||
{
|
||||
public required string DocumentNumber { get; set; }
|
||||
|
||||
public required DateTime BirthDate { get; set; }
|
||||
|
||||
public required string PhoneNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The player's health insurance provider or social work plan.
|
||||
/// </summary>
|
||||
public required string SocialSecurity { get; set; }
|
||||
|
||||
public required bool IsFederated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The club the player is affiliated with; only meaningful when IsFederated is true.
|
||||
/// </summary>
|
||||
public required string Club { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The player's federation category; only meaningful when IsFederated is true.
|
||||
/// </summary>
|
||||
public required string Category { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Application.DTOs.Player.Response;
|
||||
|
||||
/// <summary>
|
||||
/// The outcome of registering a player onto a team's roster for a season.
|
||||
/// </summary>
|
||||
public class PlayerRegistrationResponse
|
||||
{
|
||||
public required Guid PlayerId { get; set; }
|
||||
public required Guid TeamId { get; set; }
|
||||
public required Guid TournamentId { get; set; }
|
||||
public int? JerseyNumber { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
namespace Application.DTOs.Player.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response for a Player, inheriting from the base response.
|
||||
/// </summary>
|
||||
public class PublicPlayerResponse : BaseEntityResponse
|
||||
{
|
||||
public required string FirstName { get; set; }
|
||||
|
||||
public required string SecondName { get; set; }
|
||||
|
||||
public required string LastName { get; set; }
|
||||
|
||||
public required string FullName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The unique, URL-friendly identifier used in public player links.
|
||||
/// </summary>
|
||||
public required string Slug { get; set; }
|
||||
|
||||
public required Guid TeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The player's medical-record status for this season roster; null when not loaded for a specific season.
|
||||
/// </summary>
|
||||
public MedicalRecordStatus? MedicalRecordStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the player's medical record is Approved for this season roster, letting the frontend flag it.
|
||||
/// </summary>
|
||||
public bool IsHabilitado { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The player's jersey number for this season roster; null when not loaded or not assigned.
|
||||
/// </summary>
|
||||
public int? JerseyNumber { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
namespace Application.DTOs.PlayerSanction.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for submitting an appeal against a player sanction.
|
||||
/// </summary>
|
||||
public class AppealPlayerSanctionRequest
|
||||
{
|
||||
[Required]
|
||||
[StringLength(SanctionFieldLengths.LongTextMaxLength, MinimumLength = SanctionFieldLengths.LongTextMinLength)]
|
||||
public required string Reason { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.PlayerSanction.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to create a Player Sanction.
|
||||
/// </summary>
|
||||
public class CreatePlayerSanctionRequest : IValidatableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The duration in fixtures, or fechas, of the sanction.
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "The Duration field is required.")]
|
||||
public required int Duration { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "The IssuedDate field is required.")]
|
||||
public required DateTime IssuedDate { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "The Description field is required.")]
|
||||
[MaxLength(SanctionFieldLengths.DescriptionMaxLength)]
|
||||
public required string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The kind of subject the sanction targets; defaults to Player so existing clients keep working.
|
||||
/// </summary>
|
||||
public SanctionSubjectType SubjectType { get; set; } = SanctionSubjectType.Player;
|
||||
|
||||
/// <summary>
|
||||
/// The sanctioned player's identifier; required when SubjectType is Player.
|
||||
/// </summary>
|
||||
public Guid? PlayerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sanctioned team's identifier; required when SubjectType is Team.
|
||||
/// </summary>
|
||||
public Guid? TeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sanctioned staff member's name; required when SubjectType is Staff.
|
||||
/// </summary>
|
||||
[MaxLength(SanctionFieldLengths.DescriptionMaxLength)]
|
||||
public string? StaffName { get; set; }
|
||||
|
||||
public required Guid MatchId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the identity that matches the chosen subject type is supplied.
|
||||
/// </summary>
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
switch (SubjectType)
|
||||
{
|
||||
case SanctionSubjectType.Player when !PlayerId.HasValue:
|
||||
yield return new ValidationResult(
|
||||
"The PlayerId field is required for a player sanction.",
|
||||
[nameof(PlayerId)]);
|
||||
break;
|
||||
case SanctionSubjectType.Team when !TeamId.HasValue:
|
||||
yield return new ValidationResult(
|
||||
"The TeamId field is required for a team sanction.",
|
||||
[nameof(TeamId)]);
|
||||
break;
|
||||
case SanctionSubjectType.Staff when string.IsNullOrWhiteSpace(StaffName):
|
||||
yield return new ValidationResult(
|
||||
"The StaffName field is required for a staff sanction.",
|
||||
[nameof(StaffName)]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
using Application.DTOs.Abstract.Request;
|
||||
|
||||
using System;
|
||||
namespace Application.DTOs.PlayerSanction.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to get filtered player sanctions.
|
||||
/// </summary>
|
||||
public class GetPlayerSanctionsFilteredRequest : PaginatedFilterRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Overrides the base PaginatedFilterRequest default to sort newest-issued-first.
|
||||
/// </summary>
|
||||
public GetPlayerSanctionsFilteredRequest()
|
||||
{
|
||||
// String literal, not nameof, because the enclosing namespace Application.DTOs.PlayerSanction shadows the entity type; QueryableExtensions.SortBy resolves this name against the entity by reflection and matches PlayerSanction.IssuedDate.
|
||||
OrderBy = "IssuedDate";
|
||||
Order = SortOrder.Descending;
|
||||
}
|
||||
|
||||
public Guid? PlayerId { get; set; }
|
||||
|
||||
public Guid? MatchId { get; set; }
|
||||
|
||||
public Guid? TournamentId { get; set; }
|
||||
public Guid? DivisionId { get; set; }
|
||||
public Guid? StageId { get; set; }
|
||||
public Guid? TeamId { get; set; }
|
||||
|
||||
public string? Description { get; set; }
|
||||
|
||||
public DateTime? IssuedDate { get; set; }
|
||||
|
||||
public int? Duration { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
namespace Application.DTOs.PlayerSanction.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for resolving a player sanction appeal.
|
||||
/// </summary>
|
||||
public class ResolveAppealRequest
|
||||
{
|
||||
[Required]
|
||||
public required bool Accepted { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(SanctionFieldLengths.LongTextMaxLength, MinimumLength = SanctionFieldLengths.LongTextMinLength)]
|
||||
public required string Resolution { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Application.Utils.Constants.Validation;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
namespace Application.DTOs.PlayerSanction.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to update a Player Sanction.
|
||||
/// </summary>
|
||||
public class UpdatePlayerSanctionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The duration in fixtures of the sanction.
|
||||
/// </summary>
|
||||
public int? Duration { get; set; }
|
||||
|
||||
[MaxLength(SanctionFieldLengths.DescriptionMaxLength)]
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
namespace Application.DTOs.PlayerSanction.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response for a Player Sanction.
|
||||
/// </summary>
|
||||
public class PlayerSanctionResponse : BaseEntityResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The duration in fixtures, or fechas, of the sanction.
|
||||
/// </summary>
|
||||
public required int Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of fechas still to be served; zero means fully served, null means it cannot be computed.
|
||||
/// </summary>
|
||||
public int? FechasRemaining { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the sanction is still active, meaning fechas remain to be served.
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
public required DateTime IssuedDate { get; set; }
|
||||
|
||||
public required string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The unique, URL-friendly identifier used in sanction links.
|
||||
/// </summary>
|
||||
public required string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The kind of subject the sanction targets: Player, Team, or Staff.
|
||||
/// </summary>
|
||||
public SanctionSubjectType SubjectType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sanctioned player's identifier; null for team or staff sanctions.
|
||||
/// </summary>
|
||||
public Guid? PlayerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sanctioned player's full name. Null for team or staff sanctions.
|
||||
/// </summary>
|
||||
public string? PlayerFullName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sanctioned team's identifier; null unless this is a team sanction.
|
||||
/// </summary>
|
||||
public Guid? TeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sanctioned team's name. Null unless this is a team sanction.
|
||||
/// </summary>
|
||||
public string? TeamName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sanctioned staff member's name. Null unless this is a staff sanction.
|
||||
/// </summary>
|
||||
public string? StaffName { get; set; }
|
||||
|
||||
public required Guid MatchId { get; set; }
|
||||
|
||||
public SanctionAppealStatus AppealStatus { get; set; }
|
||||
|
||||
public string? AppealReason { get; set; }
|
||||
|
||||
public DateTime? AppealDate { get; set; }
|
||||
|
||||
public string? AppealResolution { get; set; }
|
||||
|
||||
public DateTime? AppealResolvedDate { get; set; }
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
namespace Application.DTOs.PlayerStatistic.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for creating a player statistic.
|
||||
/// </summary>
|
||||
public class CreatePlayerStatisticRequest
|
||||
{
|
||||
[Required]
|
||||
[Range(1, int.MaxValue, ErrorMessage = "Value must be a non-negative number.")]
|
||||
public int Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of statistic, Points or Assists, defaulting to Points.
|
||||
/// </summary>
|
||||
public StatisticType Type { get; set; } = StatisticType.Points;
|
||||
|
||||
[Required]
|
||||
public Guid MatchId { get; set; }
|
||||
|
||||
[Required]
|
||||
public Guid PlayerId { get; set; }
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using Application.DTOs.Abstract.Request;
|
||||
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
namespace Application.DTOs.PlayerStatistic.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to get filtered player statistics.
|
||||
/// </summary>
|
||||
public class GetPlayerStatisticsFilteredRequest : PaginatedFilterRequest
|
||||
{
|
||||
public Guid? MatchId { get; set; }
|
||||
|
||||
public Guid? PlayerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The team to filter statistics by, matching the statistic's player's team.
|
||||
/// </summary>
|
||||
public Guid? TeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of statistic to filter by, Points or Assists.
|
||||
/// </summary>
|
||||
public StatisticType? Type { get; set; }
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.PlayerStatistic.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request to finish a match by loading both teams' scoring sheets in one coherent operation.
|
||||
/// </summary>
|
||||
public class LoadMatchResultFromSheetsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The match being finished, assigned server-side from the route rather than sent by the client.
|
||||
/// </summary>
|
||||
public Guid MatchId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The home team's per-player points; a player may score 0 and not every roster player must be listed.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required List<PlayerScoreEntry> HomeScores { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The visitor team's per-player points.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required List<PlayerScoreEntry> VisitorScores { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether the match was decided in overtime; purely informational and defaults to false.
|
||||
/// </summary>
|
||||
public bool WentToOvertime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.PlayerStatistic.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request to load a whole team's scoring sheet for a match in one coherent operation.
|
||||
/// </summary>
|
||||
public class LoadMatchSheetRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The match whose sheet is being loaded.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid MatchId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The home or visitor team whose players are being loaded.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid TeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The per-player points, which must add up to the team's score; a player may score 0 and need not be listed.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required List<PlayerScoreEntry> Scores { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.PlayerStatistic.Request;
|
||||
|
||||
/// <summary>
|
||||
/// A single player's points within a team's match sheet.
|
||||
/// </summary>
|
||||
public class PlayerScoreEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The player who scored.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required Guid PlayerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The points the player scored in the match; may be zero.
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue, ErrorMessage = "Points must be a non-negative number.")]
|
||||
public required int Points { get; set; }
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
namespace Application.DTOs.PlayerStatistic.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to update a player statistic.
|
||||
/// </summary>
|
||||
public class UpdatePlayerStatisticRequest
|
||||
{
|
||||
[Required]
|
||||
[Range(0, int.MaxValue, ErrorMessage = "Value must be a non-negative integer.")]
|
||||
public required int Value { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Application.DTOs.Abstract.Response;
|
||||
|
||||
using Domain.Enums;
|
||||
|
||||
using System;
|
||||
namespace Application.DTOs.PlayerStatistic.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Response DTO for player statistic.
|
||||
/// </summary>
|
||||
public class PlayerStatisticResponse : BaseEntityResponse
|
||||
{
|
||||
public Guid PlayerId { get; set; }
|
||||
|
||||
public int Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of statistic, Points or Assists.
|
||||
/// </summary>
|
||||
public StatisticType Type { get; set; }
|
||||
|
||||
public Guid MatchId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date of the associated match, for display without a separate lookup.
|
||||
/// </summary>
|
||||
public DateTime? MatchDate { get; set; }
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DTOs.PointDeductions.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Request to apply a disciplinary point deduction to a team within a division taken from the route.
|
||||
/// </summary>
|
||||
public class CreatePointDeductionRequest
|
||||
{
|
||||
[Required(ErrorMessage = "The TeamId field is required.")]
|
||||
public required Guid TeamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The positive amount of table points to subtract from the team's total.
|
||||
/// </summary>
|
||||
[Range(1, int.MaxValue, ErrorMessage = "Points must be at least 1.")]
|
||||
public required int Points { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The disciplinary reason for the deduction.
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "The Reason field is required.")]
|
||||
[MaxLength(300, ErrorMessage = "Reason cannot exceed 300 characters.")]
|
||||
public required string Reason { get; set; }
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
namespace Application.DTOs.PointDeductions.Response;
|
||||
|
||||
/// <summary>
|
||||
/// The point-deduction summary carried on a standings position row for a penalised team.
|
||||
/// </summary>
|
||||
public class AppliedPointDeductionResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The total table points subtracted from the team.
|
||||
/// </summary>
|
||||
public int Points { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The combined disciplinary reasons.
|
||||
/// </summary>
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user