-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1379d9b
commit ab48e0e
Showing
13 changed files
with
302 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
src/Gameboard.Api/Features/Teams/Requests/AddToTeam/AddToTeam.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Gameboard.Api.Common.Services; | ||
using Gameboard.Api.Data; | ||
using Gameboard.Api.Features.Games; | ||
using Gameboard.Api.Features.Player; | ||
using Gameboard.Api.Services; | ||
using Gameboard.Api.Structure.MediatR; | ||
using MediatR; | ||
using Microsoft.EntityFrameworkCore; | ||
using ServiceStack; | ||
|
||
namespace Gameboard.Api.Features.Teams; | ||
|
||
public record AddToTeamCommand(string TeamId, string UserId) : IRequest<AddToTeamResponse>; | ||
|
||
internal sealed class AddToTeamCommandHandler | ||
( | ||
IActingUserService actingUser, | ||
PlayerService playerService, | ||
IStore store, | ||
ITeamService teamService, | ||
IValidatorService validator | ||
) : IRequestHandler<AddToTeamCommand, AddToTeamResponse> | ||
{ | ||
private readonly IActingUserService _actingUser = actingUser; | ||
private readonly PlayerService _playerService = playerService; | ||
private readonly IStore _store = store; | ||
private readonly ITeamService _teamService = teamService; | ||
private readonly IValidatorService _validator = validator; | ||
|
||
public async Task<AddToTeamResponse> Handle(AddToTeamCommand request, CancellationToken cancellationToken) | ||
{ | ||
await _validator | ||
.Auth | ||
( | ||
c => c | ||
.RequirePermissions(Users.PermissionKey.Teams_Enroll) | ||
.Unless | ||
( | ||
async () => await _store | ||
.WithNoTracking<Data.Player>() | ||
.Where(p => p.TeamId == request.TeamId && p.Role == PlayerRole.Manager) | ||
.Where(p => p.UserId == _actingUser.Get().Id) | ||
.AnyAsync(cancellationToken) | ||
) | ||
) | ||
.AddValidator(async ctx => | ||
{ | ||
// team hasn't started playing | ||
var team = await _teamService.GetTeam(request.TeamId); | ||
if (team.SessionBegin.IsNotEmpty()) | ||
{ | ||
ctx.AddValidationException(new SessionAlreadyStarted(request.TeamId)); | ||
} | ||
|
||
// team's current roster has to be < max | ||
var gameId = await _teamService.GetGameId(request.TeamId, cancellationToken); | ||
var maxTeamSize = await _store | ||
.WithNoTracking<Data.Game>() | ||
.Where(g => g.Id == gameId) | ||
.Select(g => g.MaxTeamSize) | ||
.SingleAsync(cancellationToken); | ||
|
||
if (team.Members.Count() >= maxTeamSize) | ||
{ | ||
ctx.AddValidationException(new TeamIsFull(new SimpleEntity { Id = team.TeamId, Name = team.ApprovedName }, team.Members.Count(), maxTeamSize)); | ||
} | ||
|
||
// if the player is joining a competitive team, they can't have played this game | ||
// competitively before | ||
if (team.Mode == PlayerMode.Competition) | ||
{ | ||
var priorPlayer = await _store | ||
.WithNoTracking<Data.Player>() | ||
.Where(p => p.UserId == request.UserId && p.TeamId != team.TeamId) | ||
.Where(p => p.GameId == team.GameId) | ||
.Where(p => p.Mode == PlayerMode.Competition) | ||
.WhereDateIsNotEmpty(p => p.SessionBegin) | ||
.Select(p => new | ||
{ | ||
p.Id, | ||
p.ApprovedName, | ||
Game = new SimpleEntity { Id = p.GameId, Name = p.Game.Name }, | ||
User = new SimpleEntity { Id = p.UserId, Name = p.User.ApprovedName }, | ||
p.SessionBegin, | ||
p.TeamId | ||
}) | ||
.SingleOrDefaultAsync(cancellationToken); | ||
|
||
if (priorPlayer is not null) | ||
{ | ||
ctx.AddValidationException(new UserAlreadyPlayed(priorPlayer.User, priorPlayer.Game, priorPlayer.TeamId, priorPlayer.SessionBegin)); | ||
} | ||
} | ||
}) | ||
// | ||
.Validate(cancellationToken); | ||
|
||
// first find the team they're meant to join | ||
var team = await _teamService.GetTeam(request.TeamId); | ||
|
||
// first ensure the person is enrolled | ||
var existingPlayerId = await _store | ||
.WithNoTracking<Data.Player>() | ||
.Where(p => p.UserId == request.UserId) | ||
.Where(p => p.TeamId != request.TeamId) | ||
.WhereDateIsEmpty(p => p.SessionBegin) | ||
.Where(p => p.GameId == team.GameId) | ||
.Select(p => p.Id) | ||
.SingleOrDefaultAsync(cancellationToken); | ||
|
||
if (existingPlayerId.IsEmpty()) | ||
{ | ||
var existingPlayer = await _playerService.Enroll | ||
( | ||
new NewPlayer { GameId = team.GameId, UserId = request.UserId }, | ||
_actingUser.Get(), | ||
cancellationToken | ||
); | ||
|
||
existingPlayerId = existingPlayer.Id; | ||
} | ||
|
||
var players = await _teamService.AddPlayers(request.TeamId, cancellationToken, existingPlayerId); | ||
var addedPlayer = players.Single(); | ||
|
||
return new AddToTeamResponse | ||
{ | ||
Game = new SimpleEntity { Id = addedPlayer.GameId, Name = addedPlayer.GameName }, | ||
Player = new SimpleEntity { Id = addedPlayer.Id, Name = addedPlayer.ApprovedName }, | ||
Team = new SimpleEntity { Id = team.TeamId, Name = team.ApprovedName }, | ||
User = new SimpleEntity { Id = addedPlayer.UserId, Name = addedPlayer.UserApprovedName } | ||
}; | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
src/Gameboard.Api/Features/Teams/Requests/AddToTeam/AddToTeamModels.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
using System; | ||
using Gameboard.Api.Structure; | ||
|
||
namespace Gameboard.Api.Features.Teams; | ||
|
||
public sealed class AddToTeamResponse | ||
{ | ||
public required SimpleEntity Game { get; set; } | ||
public required SimpleEntity Player { get; set; } | ||
public required SimpleEntity Team { get; set; } | ||
public required SimpleEntity User { get; set; } | ||
} | ||
|
||
internal sealed class UserAlreadyPlayed : GameboardValidationException | ||
{ | ||
public UserAlreadyPlayed(SimpleEntity user, SimpleEntity game, string teamId, DateTimeOffset sessionStart) | ||
: base($"""User "{user.Name}" already played game {game.Name} on {sessionStart} (team {teamId})""") { } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
src/Gameboard.Api/Features/Teams/Requests/RemoveFromTeam/RemoveFromTeam.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Gameboard.Api.Common.Services; | ||
using Gameboard.Api.Data; | ||
using Gameboard.Api.Features.Player; | ||
using Gameboard.Api.Features.Users; | ||
using Gameboard.Api.Structure.MediatR; | ||
using Gameboard.Api.Structure.MediatR.Validators; | ||
using MediatR; | ||
using Microsoft.EntityFrameworkCore; | ||
|
||
namespace Gameboard.Api.Features.Teams; | ||
|
||
public record RemoveFromTeamCommand(string PlayerId) : IRequest<RemoveFromTeamResponse>; | ||
|
||
internal sealed class RemoveFromTeamHandler | ||
( | ||
IGuidService guids, | ||
EntityExistsValidator<Data.Player> playerExists, | ||
IStore store, | ||
IValidatorService validatorService | ||
) : IRequestHandler<RemoveFromTeamCommand, RemoveFromTeamResponse> | ||
{ | ||
private readonly IGuidService _guids = guids; | ||
private readonly EntityExistsValidator<Data.Player> _playerExists = playerExists; | ||
private readonly IStore _store = store; | ||
private readonly IValidatorService _validator = validatorService; | ||
|
||
public async Task<RemoveFromTeamResponse> Handle(RemoveFromTeamCommand request, CancellationToken cancellationToken) | ||
{ | ||
await _validator | ||
.Auth(c => c.RequirePermissions(PermissionKey.Teams_Enroll)) | ||
.AddValidator(_playerExists.UseValue(request.PlayerId)) | ||
.AddValidator(async ctx => | ||
{ | ||
var playerData = await _store | ||
.WithNoTracking<Data.Player>() | ||
.Where(p => p.Id == request.PlayerId) | ||
.Select(p => new | ||
{ | ||
p.Id, | ||
p.ApprovedName, | ||
p.SessionBegin, | ||
p.Role, | ||
p.TeamId | ||
}) | ||
.SingleOrDefaultAsync(cancellationToken); | ||
|
||
// if they started the session already, tough nuggets | ||
if (!playerData.SessionBegin.IsEmpty()) | ||
{ | ||
ctx.AddValidationException(new SessionAlreadyStarted(request.PlayerId, "This player can't be removed from the team.")); | ||
} | ||
|
||
// you can't remove the captain (unenroll them instead) | ||
if (playerData.Role == PlayerRole.Manager) | ||
{ | ||
ctx.AddValidationException(new CantRemoveCaptain(new SimpleEntity { Id = playerData.Id, Name = playerData.ApprovedName }, playerData.TeamId)); | ||
} | ||
|
||
// in theory the last remaining player should be the captain and should get caught by above, | ||
// but because the schema is weird (shoutout #553), check anyway | ||
var hasRemainingTeammates = await _store | ||
.WithNoTracking<Data.Player>() | ||
.Where(p => p.TeamId == playerData.TeamId) | ||
.Where(p => p.Id != request.PlayerId) | ||
.AnyAsync(cancellationToken); | ||
|
||
if (!hasRemainingTeammates) | ||
{ | ||
ctx.AddValidationException(new CantRemoveLastTeamMember(new SimpleEntity { Id = playerData.Id, Name = playerData.ApprovedName }, playerData.TeamId)); | ||
} | ||
}) | ||
.Validate(cancellationToken); | ||
|
||
await _store | ||
.WithNoTracking<Data.Player>() | ||
.Where(p => p.Id == request.PlayerId) | ||
.ExecuteUpdateAsync(up => up.SetProperty(p => p.TeamId, _guids.Generate()), cancellationToken); | ||
|
||
return await _store | ||
.WithNoTracking<Data.Player>() | ||
.Where(p => p.Id == request.PlayerId) | ||
.Select(p => new RemoveFromTeamResponse | ||
{ | ||
Player = new SimpleEntity { Id = p.Id, Name = p.ApprovedName }, | ||
Game = new SimpleEntity { Id = p.GameId, Name = p.Game.Name }, | ||
TeamId = p.TeamId, | ||
UserId = new SimpleEntity { Id = p.UserId, Name = p.User.ApprovedName } | ||
}) | ||
.SingleAsync(cancellationToken); | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
src/Gameboard.Api/Features/Teams/Requests/RemoveFromTeam/RemoveFromTeamModels.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace Gameboard.Api.Features.Teams; | ||
|
||
public record RemoveFromTeamResponse | ||
{ | ||
public required SimpleEntity Game { get; set; } | ||
public required SimpleEntity Player { get; set; } | ||
public required string TeamId { get; set; } | ||
public required SimpleEntity UserId { get; set; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.