From 4f5473c31b78c3651c650a49c9c78f66c3b2ffe9 Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Tue, 18 Feb 2025 12:08:03 -0500 Subject: [PATCH] Resolve #415 --- .../Challenges/ChallengeSyncServiceTests.cs | 5 ++++ .../Features/Challenge/ChallengeController.cs | 4 +++ .../Requests/SyncChallenge/SyncChallenge.cs | 27 +++++++++++++++++++ .../Services/ChallengeSyncService.cs | 20 ++++++++++++++ .../EnrollmentReportService.cs | 7 +++-- 5 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 src/Gameboard.Api/Features/Challenge/Requests/SyncChallenge/SyncChallenge.cs diff --git a/src/Gameboard.Api.Tests.Unit/Tests/Features/Challenges/ChallengeSyncServiceTests.cs b/src/Gameboard.Api.Tests.Unit/Tests/Features/Challenges/ChallengeSyncServiceTests.cs index f77f83b3..e9d066fa 100644 --- a/src/Gameboard.Api.Tests.Unit/Tests/Features/Challenges/ChallengeSyncServiceTests.cs +++ b/src/Gameboard.Api.Tests.Unit/Tests/Features/Challenges/ChallengeSyncServiceTests.cs @@ -27,6 +27,7 @@ public async Task GetExpiredChallengesForSync_WithPlayerWithNullishEndDate_DoesN var store = BuildTestableStore(challenge); var sut = new ChallengeSyncService ( + A.Fake(), A.Fake(), A.Fake(), A.Fake>(), @@ -59,6 +60,7 @@ public async Task GetExpiredChallengesForSync_WithPlayerSessionEndInFuture_DoesN var store = BuildTestableStore(challenge); var sut = new ChallengeSyncService ( + A.Fake(), A.Fake(), A.Fake(), A.Fake>(), @@ -91,6 +93,7 @@ public async Task GetExpiredChallengesForSync_WithChallengeAlreadySynced_DoesNot var store = BuildTestableStore(challenge); var sut = new ChallengeSyncService ( + A.Fake(), A.Fake(), A.Fake(), A.Fake>(), @@ -123,6 +126,7 @@ public async Task GetExpiredChallengesForSync_WithNonNullishEndDate_DoesNotSync( var store = BuildTestableStore(challenge); var sut = new ChallengeSyncService ( + A.Fake(), A.Fake(), A.Fake(), A.Fake>(), @@ -155,6 +159,7 @@ public async Task GetExpiredChallengesForSync_WithAllRequiredCriteriaAndNullishE var store = BuildTestableStore(challenge); var sut = new ChallengeSyncService ( + A.Fake(), A.Fake(), A.Fake(), A.Fake>(), diff --git a/src/Gameboard.Api/Features/Challenge/ChallengeController.cs b/src/Gameboard.Api/Features/Challenge/ChallengeController.cs index 4818d2a2..0fc43594 100644 --- a/src/Gameboard.Api/Features/Challenge/ChallengeController.cs +++ b/src/Gameboard.Api/Features/Challenge/ChallengeController.cs @@ -194,6 +194,10 @@ await Hub.Clients.Group(result.TeamId).ChallengeEvent return result; } + [HttpPut("/api/challenge/{challengeId}/sync")] + public Task Sync([FromRoute] string challengeId, CancellationToken cancellationToken) + => _mediator.Send(new SyncChallengeCommand(challengeId), cancellationToken); + /// /// Grade a challenge /// diff --git a/src/Gameboard.Api/Features/Challenge/Requests/SyncChallenge/SyncChallenge.cs b/src/Gameboard.Api/Features/Challenge/Requests/SyncChallenge/SyncChallenge.cs new file mode 100644 index 00000000..e2ac4dbb --- /dev/null +++ b/src/Gameboard.Api/Features/Challenge/Requests/SyncChallenge/SyncChallenge.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using Gameboard.Api.Features.GameEngine; +using Gameboard.Api.Features.Users; +using Gameboard.Api.Structure.MediatR; +using MediatR; + +namespace Gameboard.Api.Features.Challenges; + +public record SyncChallengeCommand(string ChallengeId) : IRequest; + +internal sealed class SyncChallengeHandler +( + IChallengeSyncService challengeSyncService, + IValidatorService validator +) : IRequestHandler +{ + public async Task Handle(SyncChallengeCommand request, CancellationToken cancellationToken) + { + await validator + .Auth(c => c.Require(PermissionKey.Admin_View)) + .AddEntityExistsValidator(request.ChallengeId) + .Validate(cancellationToken); + + return await challengeSyncService.Sync(request.ChallengeId, cancellationToken); + } +} diff --git a/src/Gameboard.Api/Features/Challenge/Services/ChallengeSyncService.cs b/src/Gameboard.Api/Features/Challenge/Services/ChallengeSyncService.cs index 35700a7b..2208ba4f 100644 --- a/src/Gameboard.Api/Features/Challenge/Services/ChallengeSyncService.cs +++ b/src/Gameboard.Api/Features/Challenge/Services/ChallengeSyncService.cs @@ -16,6 +16,7 @@ namespace Gameboard.Api.Features.Challenges; public interface IChallengeSyncService { + Task Sync(string challengeId, CancellationToken cancellationToken); Task Sync(Data.Challenge challenge, GameEngineGameState challengeState, string actingUserId, CancellationToken cancellationToken); Task SyncExpired(CancellationToken cancellationToken); } @@ -25,6 +26,7 @@ public interface IChallengeSyncService /// internal class ChallengeSyncService ( + IActingUserService actingUser, ConsoleActorMap consoleActorMap, IGameEngineService gameEngine, ILogger logger, @@ -33,6 +35,7 @@ internal class ChallengeSyncService IStore store ) : IChallengeSyncService { + private readonly IActingUserService _actingUser = actingUser; private readonly ConsoleActorMap _consoleActorMap = consoleActorMap; private readonly IGameEngineService _gameEngine = gameEngine; private readonly ILogger _logger = logger; @@ -40,6 +43,23 @@ IStore store private readonly INowService _now = now; private readonly IStore _store = store; + public async Task Sync(string challengeId, CancellationToken cancellationToken) + { + var challenge = await _store + .WithNoTracking() + .SingleOrDefaultAsync(c => c.Id == challengeId, cancellationToken); + + if (challenge is null) + { + return null; + } + + var state = await _gameEngine.GetChallengeState(GameEngineType.TopoMojo, challenge.State); + await Sync(challenge, state, _actingUser.Get()?.Id, cancellationToken); + + return await _gameEngine.GetChallengeState(GameEngineType.TopoMojo, challenge.State); + } + public Task Sync(Data.Challenge challenge, GameEngineGameState state, string actingUserId, CancellationToken cancellationToken) => Sync(cancellationToken, new SyncEntry(actingUserId, challenge, state)); diff --git a/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportService.cs b/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportService.cs index fa0a3448..58951c2f 100644 --- a/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportService.cs +++ b/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportService.cs @@ -16,8 +16,7 @@ public interface IEnrollmentReportService Task GetSummaryStats(EnrollmentReportParameters parameters, CancellationToken cancellationToken); } -internal class EnrollmentReportService( - IReportsService reportsService, +internal class EnrollmentReportService(IReportsService reportsService, IStore store ) : IEnrollmentReportService { @@ -32,8 +31,8 @@ IStore store var seriesCriteria = _reportsService.ParseMultiSelectCriteria(parameters.Series); var sponsorCriteria = _reportsService.ParseMultiSelectCriteria(parameters.Sponsors); var trackCriteria = _reportsService.ParseMultiSelectCriteria(parameters.Tracks); - DateTimeOffset? enrollDateStart = parameters.EnrollDateStart.HasValue ? parameters.EnrollDateStart.Value.ToEndDate().ToUniversalTime() : null; - DateTimeOffset? enrollDateEnd = parameters.EnrollDateEnd.HasValue ? parameters.EnrollDateEnd.Value.ToEndDate().ToUniversalTime() : null; + var enrollDateStart = parameters.EnrollDateStart.HasValue ? parameters.EnrollDateStart.Value.ToEndDate().ToUniversalTime() : default(DateTimeOffset?); + var enrollDateEnd = parameters.EnrollDateEnd.HasValue ? parameters.EnrollDateEnd.Value.ToEndDate().ToUniversalTime() : default(DateTimeOffset?); // the fundamental unit of reporting here is really the player record (an "enrollment"), so resolve enrollments that // meet the filter criteria (and have at least one challenge completed in competitive mode)