DeutschLernen/GermanApp/Application/Services/LevelCompletionCalculator.cs
Lasse Rune Hansen 9215ed7a05 feat(backend/application): Complete Lesson Management feature implementation
- 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>
2026-06-13 09:34:42 +02:00

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;
}
}