Add clinical progression ranks and records

This commit is contained in:
hz
2026-08-01 15:17:42 -05:00
parent bc9f68aaf2
commit 8c5aa17d89
21 changed files with 631 additions and 10 deletions
@@ -8,7 +8,7 @@ namespace QuestionsApi.Controllers;
[ApiController]
[Route("api/dashboard")]
[Authorize(Roles = "USER,SUPER")]
public sealed class DashboardController(DashboardStore dashboard) : ControllerBase
public sealed class DashboardController(DashboardStore dashboard, ProgressionStore progression) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get()
@@ -18,6 +18,8 @@ public sealed class DashboardController(DashboardStore dashboard) : ControllerBa
?? User.Identity?.Name
?? "";
if (string.IsNullOrWhiteSpace(userId)) return Unauthorized();
return Ok(await dashboard.GetAsync(userId, DateTime.UtcNow));
var response = await dashboard.GetAsync(userId, DateTime.UtcNow);
response.Progression = await progression.GetAsync(userId);
return Ok(response);
}
}
@@ -13,7 +13,8 @@ public class ExamSimulationsController(
ExamSimulationStore simulations,
CreditStore credits,
MasteryAwardService masteryAwards,
StudySessionStore studySessions) : ControllerBase
StudySessionStore studySessions,
ProgressionStore progression) : ControllerBase
{
[HttpGet("readiness")]
public async Task<IActionResult> Readiness() => Ok(await simulations.GetReadinessAsync(UserId()));
@@ -145,6 +146,7 @@ public class ExamSimulationsController(
{
var userId = UserId();
await masteryAwards.PrepareAsync(userId);
await progression.PrepareAsync(userId);
simulation = await simulations.SubmitBlockAsync(simulation);
var response = await simulations.ToResponseAsync(simulation);
var session = await studySessions.CompleteSimulationBlockAsync(userId, simulation.Id ?? id, simulation.CurrentBlock);
@@ -164,6 +166,10 @@ public class ExamSimulationsController(
}
else if (session.Summary is not null)
response.Credits = await credits.GetStatusAsync(userId);
var progressionResult = await progression.RefreshAsync(userId);
response.Progression = progressionResult.State;
response.CreditAwards.AddRange(progressionResult.Awards);
if (progressionResult.Awards.Count > 0) response.Credits = await credits.GetStatusAsync(userId);
return Ok(response);
}
catch (InvalidOperationException exception) { return Conflict(exception.Message); }
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QuestionsApi.Services;
using System.Security.Claims;
namespace QuestionsApi.Controllers;
[ApiController, Route("api/progression"), Authorize(Roles = "USER,SUPER")]
public sealed class ProgressionController(ProgressionStore progression) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.Name)
?? User.Identity?.Name ?? "";
return string.IsNullOrWhiteSpace(userId) ? Unauthorized() : Ok(await progression.GetAsync(userId));
}
}
@@ -23,6 +23,7 @@ public class QuestionsController : ControllerBase
private readonly MasteryAwardService _masteryAwards;
private readonly StudySessionStore _studySessions;
private readonly DailyMissionStore _dailyMissions;
private readonly ProgressionStore _progression;
public QuestionsController(
QuestionStore questions,
@@ -36,7 +37,8 @@ public class QuestionsController : ControllerBase
CreditStore credits,
MasteryAwardService masteryAwards,
StudySessionStore studySessions,
DailyMissionStore dailyMissions)
DailyMissionStore dailyMissions,
ProgressionStore progression)
{
_questions = questions;
_attempts = attempts;
@@ -50,6 +52,7 @@ public class QuestionsController : ControllerBase
_masteryAwards = masteryAwards;
_studySessions = studySessions;
_dailyMissions = dailyMissions;
_progression = progression;
}
[HttpGet("systems")]
@@ -162,6 +165,7 @@ public class QuestionsController : ControllerBase
return Unauthorized();
await _masteryAwards.PrepareAsync(userId);
await _progression.PrepareAsync(userId);
var record = await _attempts.RecordFirstAttemptAsync(
question.ExternalId,
@@ -237,6 +241,8 @@ public class QuestionsController : ControllerBase
&& creditAwards.All(existing => existing.ReasonCode != value.ReasonCode)));
var dailyMissions = await _dailyMissions.RecordQuestionAsync(userId, record.Attempt, question);
if (dailyMissions is not null) creditAwards.AddRange(dailyMissions.CreditAwards);
var progression = await _progression.RefreshAsync(userId);
creditAwards.AddRange(progression.Awards);
return Ok(new FirstQuestionAttemptResponse
{
QuestionExternalId = question.ExternalId,
@@ -249,7 +255,8 @@ public class QuestionsController : ControllerBase
CreditAwards = creditAwards,
StudySessionProgress = studySession?.Progress,
SessionSummary = studySession?.Summary,
DailyMissions = dailyMissions?.State
DailyMissions = dailyMissions?.State,
Progression = progression.State
});
}
@@ -310,6 +317,8 @@ public class QuestionsController : ControllerBase
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
await _progression.PrepareAsync(userId);
var record = await _attempts.RecordFirstAttemptAsync(
question.ExternalId,
userId,
@@ -342,6 +351,7 @@ public class QuestionsController : ControllerBase
throw;
}
var progression = await _progression.RefreshAsync(userId);
return Ok(new SkipQuestionResponse
{
QuestionExternalId = question.ExternalId,
@@ -350,7 +360,8 @@ public class QuestionsController : ControllerBase
question,
await _statistics.GetPercentagesForQuestionAsync(question.ExternalId)),
QuestionElo = rating.QuestionElo,
Rating = rating
Rating = rating,
Progression = progression.State
});
}
@@ -18,7 +18,8 @@ public sealed class ReviewQueueController(
CreditStore credits,
MasteryAwardService masteryAwards,
StudySessionStore studySessions,
DailyMissionStore dailyMissions) : ControllerBase
DailyMissionStore dailyMissions,
ProgressionStore progression) : ControllerBase
{
[HttpPost("next")]
public async Task<IActionResult> Next([FromBody] ReviewQueueNextRequest req)
@@ -76,6 +77,7 @@ public sealed class ReviewQueueController(
}
await masteryAwards.PrepareAsync(userId);
await progression.PrepareAsync(userId);
var result = await queue.RecordReviewAsync(
userId,
req.ClientAttemptId,
@@ -110,6 +112,8 @@ public sealed class ReviewQueueController(
&& awards.All(existing => existing.ReasonCode != value.ReasonCode)));
var missionResult = await dailyMissions.RecordReviewAsync(userId, result.Attempt);
if (missionResult is not null) awards.AddRange(missionResult.CreditAwards);
var progressionResult = await progression.RefreshAsync(userId);
awards.AddRange(progressionResult.Awards);
return Ok(new ReviewQueueAttemptResponse
{
QuestionExternalId = question.ExternalId,
@@ -120,7 +124,8 @@ public sealed class ReviewQueueController(
CreditAwards = awards,
StudySessionProgress = studySession?.Progress,
SessionSummary = studySession?.Summary,
DailyMissions = missionResult?.State
DailyMissions = missionResult?.State,
Progression = progressionResult.State
});
}