- 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>
110 lines
4.3 KiB
C#
110 lines
4.3 KiB
C#
using GermanApp.Domain.Interfaces;
|
|
|
|
namespace GermanApp.Application.Services;
|
|
|
|
/// <summary>
|
|
/// Domain service for managing lesson unlocking business logic.
|
|
/// This is part of the Application layer.
|
|
/// </summary>
|
|
public class LessonUnlockService
|
|
{
|
|
private readonly ILessonRepository _lessonRepository;
|
|
private readonly IUserProgressRepository _userProgressRepository;
|
|
|
|
public LessonUnlockService(
|
|
ILessonRepository lessonRepository,
|
|
IUserProgressRepository userProgressRepository)
|
|
{
|
|
_lessonRepository = lessonRepository;
|
|
_userProgressRepository = userProgressRepository;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the next lesson in a level is unlocked for the user.
|
|
/// A lesson is unlocked if:
|
|
/// - It's the first lesson in the level, OR
|
|
/// - The previous lesson in the level has been completed
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="currentLessonId">The current lesson ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>True if the next lesson is unlocked, false otherwise</returns>
|
|
public virtual async Task<bool> IsNextLessonUnlockedAsync(
|
|
int userId,
|
|
int currentLessonId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var currentLesson = await _lessonRepository.GetByIdAsync(currentLessonId, cancellationToken);
|
|
if (currentLesson == null)
|
|
return false;
|
|
|
|
// If this is the first lesson in the level, the next one is always unlocked
|
|
var firstLesson = await _lessonRepository.GetFirstLessonInLevelAsync(
|
|
currentLesson.LevelId,
|
|
cancellationToken);
|
|
|
|
if (firstLesson?.Id == currentLessonId)
|
|
{
|
|
var nextLesson = await _lessonRepository.GetNextLessonAsync(currentLessonId, cancellationToken);
|
|
return nextLesson != null;
|
|
}
|
|
|
|
// Check if the user has completed the current lesson
|
|
var hasCompleted = await _userProgressRepository.HasUserCompletedLessonAsync(
|
|
userId,
|
|
currentLessonId,
|
|
cancellationToken);
|
|
|
|
return hasCompleted;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the next lesson ID that should be unlocked for a user after completing a lesson.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="completedLessonId">The ID of the lesson that was just completed</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>The ID of the next lesson, or null if no more lessons in the level</returns>
|
|
public virtual async Task<int?> GetNextLessonIdAsync(
|
|
int userId,
|
|
int completedLessonId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var currentLesson = await _lessonRepository.GetByIdAsync(completedLessonId, cancellationToken);
|
|
if (currentLesson == null)
|
|
return null;
|
|
|
|
var nextLesson = await _lessonRepository.GetNextLessonAsync(completedLessonId, cancellationToken);
|
|
return nextLesson?.Id;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all lessons that are accessible to a user (completed lessons + next unlocked lesson).
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of accessible lesson IDs</returns>
|
|
public virtual async Task<IReadOnlyList<int>> GetAccessibleLessonIdsAsync(
|
|
int userId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var lessons = await _lessonRepository.GetAccessibleLessonsAsync(userId, cancellationToken);
|
|
return lessons.Select(l => l.Id).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a specific lesson is accessible to a user.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="lessonId">The lesson ID to check</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>True if the lesson is accessible, false otherwise</returns>
|
|
public virtual async Task<bool> IsLessonAccessibleAsync(
|
|
int userId,
|
|
int lessonId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var accessibleLessons = await GetAccessibleLessonIdsAsync(userId, cancellationToken);
|
|
return accessibleLessons.Contains(lessonId);
|
|
}
|
|
}
|