- Integrated Quiz completion with ProgressService: when a quiz is passed (>=80%), the associated lesson is automatically marked as completed - Created LessonUnlockService for centralized lesson unlocking business logic - Created LevelCompletionCalculator for calculating level completion metrics - Registered new services in Program.cs DI container - Updated QuizQuestionService to include ProgressService dependency - Updated documentation (ROADMAP.md, lesson-management.md) to reflect completion - Fixed QuizQuestionServiceTests to work with updated dependencies All tests pass (296 total: 148 unit + 148 integration). Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
190 lines
6.7 KiB
C#
190 lines
6.7 KiB
C#
using GermanApp.Application.DTOs;
|
|
using GermanApp.Domain.Interfaces;
|
|
|
|
namespace GermanApp.Application.Services;
|
|
|
|
/// <summary>
|
|
/// Service for calculating level completion metrics.
|
|
/// This is part of the Application layer.
|
|
/// </summary>
|
|
public class LevelCompletionCalculator
|
|
{
|
|
private readonly ILevelRepository _levelRepository;
|
|
private readonly ILessonRepository _lessonRepository;
|
|
private readonly IUserProgressRepository _userProgressRepository;
|
|
|
|
public LevelCompletionCalculator(
|
|
ILevelRepository levelRepository,
|
|
ILessonRepository lessonRepository,
|
|
IUserProgressRepository userProgressRepository)
|
|
{
|
|
_levelRepository = levelRepository;
|
|
_lessonRepository = lessonRepository;
|
|
_userProgressRepository = userProgressRepository;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the completion percentage for a specific level for a user.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="levelId">The level ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Completion percentage (0-100)</returns>
|
|
public virtual async Task<double> CalculateLevelCompletionPercentageAsync(
|
|
int userId,
|
|
int levelId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
// Get total lessons in the level
|
|
var lessons = await _lessonRepository.GetByLevelAsync(levelId, cancellationToken);
|
|
var totalLessons = lessons.Count;
|
|
|
|
if (totalLessons == 0)
|
|
return 0;
|
|
|
|
// Get completed lessons count for user in this level
|
|
var completedCount = await _userProgressRepository.GetLevelCompletionPercentageAsync(
|
|
userId,
|
|
levelId,
|
|
cancellationToken);
|
|
|
|
// The repository method returns percentage, but we need the count
|
|
// Let's recalculate properly
|
|
var completedLessons = 0;
|
|
foreach (var lesson in lessons)
|
|
{
|
|
var hasCompleted = await _userProgressRepository.HasUserCompletedLessonAsync(
|
|
userId,
|
|
lesson.Id,
|
|
cancellationToken);
|
|
if (hasCompleted)
|
|
completedLessons++;
|
|
}
|
|
|
|
return (double)completedLessons / totalLessons * 100;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets completion information for all levels for a user.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of level completion DTOs</returns>
|
|
public virtual async Task<IReadOnlyList<LevelCompletionDto>> CalculateAllLevelCompletionsAsync(
|
|
int userId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var levels = await _levelRepository.GetAllOrderedAsync(cancellationToken);
|
|
var completions = new List<LevelCompletionDto>();
|
|
|
|
foreach (var level in levels)
|
|
{
|
|
var lessons = await _lessonRepository.GetByLevelAsync(level.Id, cancellationToken);
|
|
var totalLessons = lessons.Count;
|
|
|
|
var completedCount = 0;
|
|
foreach (var lesson in lessons)
|
|
{
|
|
var hasCompleted = await _userProgressRepository.HasUserCompletedLessonAsync(
|
|
userId,
|
|
lesson.Id,
|
|
cancellationToken);
|
|
if (hasCompleted)
|
|
completedCount++;
|
|
}
|
|
|
|
var percentage = totalLessons > 0 ? (double)completedCount / totalLessons * 100 : 0;
|
|
|
|
completions.Add(new LevelCompletionDto(
|
|
level.Id,
|
|
level.Name,
|
|
level.Code,
|
|
totalLessons,
|
|
completedCount,
|
|
percentage
|
|
));
|
|
}
|
|
|
|
return completions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a user has completed all lessons in a level.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="levelId">The level ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>True if all lessons are completed, false otherwise</returns>
|
|
public virtual async Task<bool> IsLevelFullyCompletedAsync(
|
|
int userId,
|
|
int levelId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var percentage = await CalculateLevelCompletionPercentageAsync(userId, levelId, cancellationToken);
|
|
return percentage >= 100;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the number of completed lessons in a level for a user.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="levelId">The level ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Number of completed lessons</returns>
|
|
public virtual async Task<int> GetCompletedLessonCountAsync(
|
|
int userId,
|
|
int levelId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var lessons = await _lessonRepository.GetByLevelAsync(levelId, cancellationToken);
|
|
var completedCount = 0;
|
|
|
|
foreach (var lesson in lessons)
|
|
{
|
|
var hasCompleted = await _userProgressRepository.HasUserCompletedLessonAsync(
|
|
userId,
|
|
lesson.Id,
|
|
cancellationToken);
|
|
if (hasCompleted)
|
|
completedCount++;
|
|
}
|
|
|
|
return completedCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the number of total lessons in a level.
|
|
/// </summary>
|
|
/// <param name="levelId">The level ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Total number of lessons</returns>
|
|
public virtual async Task<int> GetTotalLessonCountAsync(
|
|
int levelId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var lessons = await _lessonRepository.GetByLevelAsync(levelId, cancellationToken);
|
|
return lessons.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the next level ID that should be unlocked for a user.
|
|
/// Returns the next level if the current level is fully completed.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="currentLevelId">The current level ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>The next level ID, or null if no more levels</returns>
|
|
public virtual async Task<int?> GetNextLevelIdAsync(
|
|
int userId,
|
|
int currentLevelId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var isCompleted = await IsLevelFullyCompletedAsync(userId, currentLevelId, cancellationToken);
|
|
|
|
if (!isCompleted)
|
|
return null;
|
|
|
|
var nextLevel = await _levelRepository.GetNextLevelAsync(currentLevelId, cancellationToken);
|
|
return nextLevel?.Id;
|
|
}
|
|
}
|