Files
GameList/Endpoints/SuggestionWorkflowService.cs

273 lines
10 KiB
C#

using GameList.Contracts;
using GameList.Data;
using GameList.Domain;
using Microsoft.EntityFrameworkCore;
namespace GameList.Endpoints;
internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFactory httpFactory)
{
public async Task<ServiceResult<IReadOnlyList<SuggestionDto>>> GetMineAsync(Guid playerId)
{
var mine = await db.Suggestions
.AsNoTracking()
.Where(s => s.PlayerId == playerId)
.Select(s => new
{
s.Id,
s.PlayerId,
s.Name,
s.Genre,
s.Description,
s.ScreenshotUrl,
s.YoutubeUrl,
s.GameUrl,
s.CreatedAt,
s.MinPlayers,
s.MaxPlayers,
s.ParentSuggestionId
})
.ToListAsync();
IReadOnlyList<SuggestionDto> ordered = mine
.OrderBy(s => s.CreatedAt)
.Select(s => new SuggestionDto(s.Id, s.Name, s.Genre, s.Description, s.ScreenshotUrl, s.YoutubeUrl, s.GameUrl, s.MinPlayers, s.MaxPlayers, s.ParentSuggestionId))
.ToList();
return ServiceResult<IReadOnlyList<SuggestionDto>>.Success(ordered);
}
public async Task<ServiceResult<SuggestionCreatedResponse>> CreateAsync(Guid playerId, SuggestionInput input)
{
var validationError = await SuggestionValidator.ValidateAsync(input, httpFactory);
if (validationError is not null)
return ServiceResult<SuggestionCreatedResponse>.Failure(ServiceError.BadRequest(validationError));
var playerState = await db.Players
.AsNoTracking()
.Where(p => p.Id == playerId)
.Select(p => new
{
p.DisplayName,
p.HasJoker
})
.FirstAsync();
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
var usingJoker = phase == Phase.Vote && playerState.HasJoker;
if (phase != Phase.Suggest && !usingJoker)
return ServiceResult<SuggestionCreatedResponse>.Failure(ServiceError.PhaseMismatch(Phase.Suggest, phase));
if (string.IsNullOrWhiteSpace(playerState.DisplayName))
return ServiceResult<SuggestionCreatedResponse>.Failure(ServiceError.BadRequest("Set a display name before submitting suggestions."));
var existingCount = await db.Suggestions.AsNoTracking().CountAsync(s => s.PlayerId == playerId);
if (!usingJoker && existingCount >= 5)
return ServiceResult<SuggestionCreatedResponse>.Failure(ServiceError.BadRequest("You have reached the 5 suggestion limit."));
var suggestion = new Suggestion
{
PlayerId = playerId,
Name = input.Name.Trim(),
Genre = EndpointHelpers.TrimTo(input.Genre, 50),
Description = EndpointHelpers.TrimTo(input.Description, 500),
ScreenshotUrl = EndpointHelpers.TrimTo(input.ScreenshotUrl, 2048),
YoutubeUrl = EndpointHelpers.TrimTo(input.YoutubeUrl, 2048),
GameUrl = EndpointHelpers.TrimTo(input.GameUrl, 2048),
MinPlayers = input.MinPlayers,
MaxPlayers = input.MaxPlayers
};
await using var tx = await db.Database.BeginTransactionAsync();
db.Suggestions.Add(suggestion);
try
{
await db.SaveChangesAsync();
if (usingJoker)
{
await db.Players
.Where(p => p.Id == playerId)
.ExecuteUpdateAsync(p => p.SetProperty(x => x.HasJoker, false));
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.VotesFinal, false));
}
await tx.CommitAsync();
}
catch (DbUpdateException ex) when (EndpointHelpers.IsSqliteConstraintViolation(ex, EndpointHelpers.SuggestionLimitTriggerError))
{
return ServiceResult<SuggestionCreatedResponse>.Failure(ServiceError.BadRequest("You have reached the 5 suggestion limit."));
}
return ServiceResult<SuggestionCreatedResponse>.Success(new SuggestionCreatedResponse(suggestion.Id));
}
public async Task<ServiceResult<Unit>> DeleteAsync(Guid playerId, int suggestionId)
{
var actor = await db.Players
.AsNoTracking()
.Where(p => p.Id == playerId)
.Select(p => new
{
p.IsAdmin
})
.FirstAsync();
var isAdmin = actor.IsAdmin;
if (!isAdmin)
{
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
if (phase != Phase.Suggest)
return ServiceResult<Unit>.Failure(ServiceError.PhaseMismatch(Phase.Suggest, phase));
}
var suggestion = isAdmin
? await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId)
: await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId && s.PlayerId == playerId);
if (suggestion == null)
return ServiceResult<Unit>.Failure(ServiceError.NotFound("Suggestion not found."));
await using var tx = await db.Database.BeginTransactionAsync();
await db.Suggestions
.Where(s => s.ParentSuggestionId == suggestion.Id)
.ExecuteUpdateAsync(s => s.SetProperty(x => x.ParentSuggestionId, (int?)null));
await db.Votes.Where(v => v.SuggestionId == suggestion.Id).ExecuteDeleteAsync();
db.Suggestions.Remove(suggestion);
await db.SaveChangesAsync();
await tx.CommitAsync();
return ServiceResult<Unit>.Success(default);
}
public async Task<ServiceResult<SuggestionUpdatedResponse>> UpdateAsync(Guid playerId, int suggestionId, SuggestionInput input)
{
var validationError = await SuggestionValidator.ValidateAsync(input, httpFactory);
if (validationError is not null)
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.BadRequest(validationError));
var actor = await db.Players
.AsNoTracking()
.Where(p => p.Id == playerId)
.Select(p => new
{
p.IsAdmin
})
.FirstAsync();
var suggestion = await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId);
if (suggestion == null)
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.NotFound("Suggestion not found."));
var isAdmin = actor.IsAdmin;
if (!isAdmin)
{
if (suggestion.PlayerId != playerId)
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.Unauthorized());
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
if (phase == Phase.Results)
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.PhaseMismatch(Phase.Suggest, phase));
if (phase == Phase.Suggest)
{
suggestion.Name = input.Name.Trim();
}
else if (phase != Phase.Vote)
{
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.PhaseMismatch(Phase.Suggest, phase));
}
ApplyEditableFields(suggestion, input);
}
else
{
suggestion.Name = input.Name.Trim();
ApplyEditableFields(suggestion, input);
}
await db.SaveChangesAsync();
return ServiceResult<SuggestionUpdatedResponse>.Success(new SuggestionUpdatedResponse(
suggestion.Id,
suggestion.Name,
suggestion.Genre,
suggestion.Description,
suggestion.ScreenshotUrl,
suggestion.YoutubeUrl,
suggestion.GameUrl,
suggestion.MinPlayers,
suggestion.MaxPlayers
));
}
public async Task<ServiceResult<IReadOnlyList<SuggestionAllDto>>> GetAllAsync(Guid playerId)
{
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
if (phase < Phase.Vote)
return ServiceResult<IReadOnlyList<SuggestionAllDto>>.Failure(ServiceError.PhaseMismatch(Phase.Vote, phase));
var all = await db.Suggestions
.AsNoTracking()
.Include(s => s.Player)
.Select(s => new
{
s.Id,
s.Name,
s.Genre,
s.Description,
s.ScreenshotUrl,
s.YoutubeUrl,
s.GameUrl,
s.MinPlayers,
s.MaxPlayers,
Author = s.Player!.DisplayName,
s.CreatedAt,
s.ParentSuggestionId,
IsOwner = s.PlayerId == playerId
})
.ToListAsync();
var rootIndex = EndpointHelpers.BuildLinkRoots(all.Select(s => (s.Id, s.ParentSuggestionId)));
var nameLookup = all.ToDictionary(s => s.Id, s => s.Name);
IReadOnlyList<SuggestionAllDto> ordered = all.OrderBy(s => s.CreatedAt).Select(s =>
{
var linkedIds = EndpointHelpers.LinkedIdsFor(s.Id, rootIndex).Where(id => id != s.Id).ToList();
return new SuggestionAllDto(
s.Id,
s.Name,
s.Genre,
s.Description,
s.ScreenshotUrl,
s.YoutubeUrl,
s.GameUrl,
s.MinPlayers,
s.MaxPlayers,
s.Author,
s.ParentSuggestionId,
s.IsOwner,
linkedIds,
linkedIds.Where(nameLookup.ContainsKey).Select(id => nameLookup[id]).ToList()
);
}).ToList();
return ServiceResult<IReadOnlyList<SuggestionAllDto>>.Success(ordered);
}
private static void ApplyEditableFields(Suggestion suggestion, SuggestionInput input)
{
suggestion.Genre = EndpointHelpers.TrimTo(input.Genre, 50);
suggestion.Description = EndpointHelpers.TrimTo(input.Description, 500);
suggestion.ScreenshotUrl = EndpointHelpers.TrimTo(input.ScreenshotUrl, 2048);
suggestion.YoutubeUrl = EndpointHelpers.TrimTo(input.YoutubeUrl, 2048);
suggestion.GameUrl = EndpointHelpers.TrimTo(input.GameUrl, 2048);
suggestion.MinPlayers = input.MinPlayers;
suggestion.MaxPlayers = input.MaxPlayers;
}
}