diff --git a/GermanApp/Application/DTOs/LevelDto.cs b/GermanApp/Application/DTOs/LevelDto.cs new file mode 100644 index 0000000..79c6a92 --- /dev/null +++ b/GermanApp/Application/DTOs/LevelDto.cs @@ -0,0 +1,51 @@ +using GermanApp.Domain.Entities; + +namespace GermanApp.Application.DTOs; + +/// +/// Data Transfer Object for Level - used for API responses. +/// This is a read-only representation of a Level entity. +/// +public record LevelDto( + int Id, + string Name, + string Code, + int Order); + +/// +/// Data Transfer Object for creating a new Level. +/// +public record CreateLevelDto( + string Name, + string Code, + int Order); + +/// +/// Data Transfer Object for updating an existing Level. +/// +public record UpdateLevelDto( + string Name, + string Code, + int Order); + +/// +/// Extension methods for mapping between Level entity and DTOs. +/// +public static class LevelDtoExtensions +{ + public static LevelDto ToDto(this Level level) => new( + level.Id, + level.Name, + level.Code, + level.Order); + + public static Level ToEntity(this CreateLevelDto dto) => + Level.Create(dto.Name, dto.Code, dto.Order); + + public static void UpdateFromDto(this Level level, UpdateLevelDto dto) + { + level.UpdateName(dto.Name); + level.UpdateCode(dto.Code); + level.UpdateOrder(dto.Order); + } +} diff --git a/GermanApp/Application/DTOs/UserProgressDto.cs b/GermanApp/Application/DTOs/UserProgressDto.cs new file mode 100644 index 0000000..233938f --- /dev/null +++ b/GermanApp/Application/DTOs/UserProgressDto.cs @@ -0,0 +1,80 @@ +using GermanApp.Domain.Entities; + +namespace GermanApp.Application.DTOs; + +/// +/// Data Transfer Object for UserProgress - used for API responses. +/// This is a read-only representation of a UserProgress entity. +/// +public record UserProgressDto( + int Id, + int UserId, + int LessonId, + bool IsCompleted, + int QuizScore, + DateTime LastAttemptDate, + string? LessonTitle = null, + string? LevelName = null, + string? LevelCode = null); + +/// +/// Data Transfer Object for creating/updating user progress. +/// +public record UpdateUserProgressDto( + int LessonId, + bool IsCompleted, + int QuizScore); + +/// +/// Data Transfer Object for level completion summary. +/// +public record LevelCompletionDto( + int LevelId, + string LevelName, + string LevelCode, + int TotalLessons, + int CompletedLessons, + double CompletionPercentage); + +/// +/// Extension methods for mapping between UserProgress entity and DTOs. +/// +public static class UserProgressDtoExtensions +{ + public static UserProgressDto ToDto(this UserProgress userProgress) => new( + userProgress.Id, + userProgress.UserId, + userProgress.LessonId, + userProgress.IsCompleted, + userProgress.QuizScore, + userProgress.LastAttemptDate, + userProgress.Lesson?.Title, + userProgress.Lesson?.Level?.Name, + userProgress.Lesson?.Level?.Code); + + public static UserProgress ToEntity(this UpdateUserProgressDto dto, int userId) + { + var progress = UserProgress.Create(userId, dto.LessonId); + if (dto.IsCompleted) + { + progress.MarkAsCompleted(dto.QuizScore); + } + else + { + progress.UpdateQuizScore(dto.QuizScore); + } + return progress; + } + + public static void UpdateFromDto(this UserProgress userProgress, UpdateUserProgressDto dto) + { + if (dto.IsCompleted) + { + userProgress.MarkAsCompleted(dto.QuizScore); + } + else + { + userProgress.UpdateQuizScore(dto.QuizScore); + } + } +} diff --git a/GermanApp/Application/Services/LessonService.cs b/GermanApp/Application/Services/LessonService.cs new file mode 100644 index 0000000..1755620 --- /dev/null +++ b/GermanApp/Application/Services/LessonService.cs @@ -0,0 +1,159 @@ +using GermanApp.Application.DTOs; +using GermanApp.Domain.Entities; +using GermanApp.Domain.Interfaces; + +namespace GermanApp.Application.Services; + +/// +/// Application service for managing lessons. +/// This is part of the Application layer. +/// +public class LessonService +{ + private readonly ILessonRepository _lessonRepository; + private readonly ILevelRepository _levelRepository; + + public LessonService(ILessonRepository lessonRepository, ILevelRepository levelRepository) + { + _lessonRepository = lessonRepository; + _levelRepository = levelRepository; + } + + /// + /// Gets all lessons. + /// + public async Task> GetAllLessonsAsync(CancellationToken cancellationToken = default) + { + var lessons = await _lessonRepository.GetAllAsync(cancellationToken); + return lessons.Select(l => l.ToDto()).ToList(); + } + + /// + /// Gets all lessons ordered by level and lesson order. + /// + public async Task> GetAllOrderedLessonsAsync(CancellationToken cancellationToken = default) + { + var lessons = await _lessonRepository.GetAllOrderedAsync(cancellationToken); + return lessons.Select(l => l.ToDto()).ToList(); + } + + /// + /// Gets a lesson by its ID. + /// + public async Task GetLessonByIdAsync(int id, CancellationToken cancellationToken = default) + { + var lesson = await _lessonRepository.GetByIdAsync(id, cancellationToken); + return lesson?.ToDto(); + } + + /// + /// Gets lessons by level ID. + /// + public async Task> GetLessonsByLevelAsync(int levelId, CancellationToken cancellationToken = default) + { + var lessons = await _lessonRepository.GetByLevelAsync(levelId, cancellationToken); + return lessons.Select(l => l.ToDto()).ToList(); + } + + /// + /// Gets beginner lessons (A1 and A2 - levels 1 and 2). + /// + public async Task> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default) + { + var level1Lessons = await _lessonRepository.GetByLevelAsync(1, cancellationToken); + var level2Lessons = await _lessonRepository.GetByLevelAsync(2, cancellationToken); + var allLessons = level1Lessons.Concat(level2Lessons) + .OrderBy(l => l.LevelId) + .ThenBy(l => l.Order) + .ToList(); + return allLessons.Select(l => l.ToDto()).ToList(); + } + + /// + /// Gets advanced lessons (B2 and C1 - levels 4 and 5). + /// + public async Task> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default) + { + var level4Lessons = await _lessonRepository.GetByLevelAsync(4, cancellationToken); + var level5Lessons = await _lessonRepository.GetByLevelAsync(5, cancellationToken); + var allLessons = level4Lessons.Concat(level5Lessons) + .OrderBy(l => l.LevelId) + .ThenBy(l => l.Order) + .ToList(); + return allLessons.Select(l => l.ToDto()).ToList(); + } + + /// + /// Creates a new lesson. + /// + public async Task CreateLessonAsync(CreateLessonDto dto, CancellationToken cancellationToken = default) + { + // Validate that the level exists + var level = await _levelRepository.GetByIdAsync(dto.LevelId, cancellationToken); + if (level == null) + throw new ArgumentException("Level does not exist"); + + var lesson = dto.ToEntity(); + var createdLesson = await _lessonRepository.AddAsync(lesson, cancellationToken); + return createdLesson.ToDto(); + } + + /// + /// Updates an existing lesson. + /// + public async Task UpdateLessonAsync(int id, UpdateLessonDto dto, CancellationToken cancellationToken = default) + { + var lesson = await _lessonRepository.GetByIdAsync(id, cancellationToken); + if (lesson == null) + return null; + + // Validate that the level exists + var level = await _levelRepository.GetByIdAsync(dto.LevelId, cancellationToken); + if (level == null) + throw new ArgumentException("Level does not exist"); + + lesson.UpdateFromDto(dto); + await _lessonRepository.UpdateAsync(lesson, cancellationToken); + return lesson.ToDto(); + } + + /// + /// Deletes a lesson by its ID. + /// + public async Task DeleteLessonAsync(int id, CancellationToken cancellationToken = default) + { + var lesson = await _lessonRepository.GetByIdAsync(id, cancellationToken); + if (lesson == null) + return false; + + await _lessonRepository.DeleteAsync(lesson, cancellationToken); + return true; + } + + /// + /// Gets the first lesson in a level. + /// + public async Task GetFirstLessonInLevelAsync(int levelId, CancellationToken cancellationToken = default) + { + var lesson = await _lessonRepository.GetFirstLessonInLevelAsync(levelId, cancellationToken); + return lesson?.ToDto(); + } + + /// + /// Gets the next lesson after the specified one. + /// + public async Task GetNextLessonAsync(int currentLessonId, CancellationToken cancellationToken = default) + { + var lesson = await _lessonRepository.GetNextLessonAsync(currentLessonId, cancellationToken); + return lesson?.ToDto(); + } + + /// + /// Gets lessons that the user can access (based on completion of previous lessons). + /// + public async Task> GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default) + { + var lessons = await _lessonRepository.GetAccessibleLessonsAsync(userId, cancellationToken); + return lessons.Select(l => l.ToDto()).ToList(); + } +} diff --git a/GermanApp/Application/Services/LevelService.cs b/GermanApp/Application/Services/LevelService.cs new file mode 100644 index 0000000..b206aa3 --- /dev/null +++ b/GermanApp/Application/Services/LevelService.cs @@ -0,0 +1,101 @@ +using GermanApp.Application.DTOs; +using GermanApp.Domain.Entities; +using GermanApp.Domain.Interfaces; + +namespace GermanApp.Application.Services; + +/// +/// Application service for managing CEFR levels. +/// This is part of the Application layer. +/// +public class LevelService +{ + private readonly ILevelRepository _levelRepository; + + public LevelService(ILevelRepository levelRepository) + { + _levelRepository = levelRepository; + } + + /// + /// Gets all CEFR levels ordered by their sort order. + /// + public async Task> GetAllLevelsAsync(CancellationToken cancellationToken = default) + { + var levels = await _levelRepository.GetAllOrderedAsync(cancellationToken); + return levels.Select(l => l.ToDto()).ToList(); + } + + /// + /// Gets a level by its ID. + /// + public async Task GetLevelByIdAsync(int id, CancellationToken cancellationToken = default) + { + var level = await _levelRepository.GetByIdAsync(id, cancellationToken); + return level?.ToDto(); + } + + /// + /// Gets a level by its code (e.g., "A1", "B2"). + /// + public async Task GetLevelByCodeAsync(string code, CancellationToken cancellationToken = default) + { + var level = await _levelRepository.GetByCodeAsync(code, cancellationToken); + return level?.ToDto(); + } + + /// + /// Creates a new CEFR level. + /// + public async Task CreateLevelAsync(CreateLevelDto dto, CancellationToken cancellationToken = default) + { + var level = dto.ToEntity(); + var createdLevel = await _levelRepository.AddAsync(level, cancellationToken); + return createdLevel.ToDto(); + } + + /// + /// Updates an existing CEFR level. + /// + public async Task UpdateLevelAsync(int id, UpdateLevelDto dto, CancellationToken cancellationToken = default) + { + var level = await _levelRepository.GetByIdAsync(id, cancellationToken); + if (level == null) + return null; + + level.UpdateFromDto(dto); + await _levelRepository.UpdateAsync(level, cancellationToken); + return level.ToDto(); + } + + /// + /// Deletes a CEFR level by its ID. + /// + public async Task DeleteLevelAsync(int id, CancellationToken cancellationToken = default) + { + var level = await _levelRepository.GetByIdAsync(id, cancellationToken); + if (level == null) + return false; + + await _levelRepository.DeleteAsync(level, cancellationToken); + return true; + } + + /// + /// Gets the first level (lowest order number) - typically A1. + /// + public async Task GetFirstLevelAsync(CancellationToken cancellationToken = default) + { + var level = await _levelRepository.GetFirstLevelAsync(cancellationToken); + return level?.ToDto(); + } + + /// + /// Gets the next level after the specified one. + /// + public async Task GetNextLevelAsync(int currentLevelId, CancellationToken cancellationToken = default) + { + var level = await _levelRepository.GetNextLevelAsync(currentLevelId, cancellationToken); + return level?.ToDto(); + } +} diff --git a/GermanApp/Application/Services/ProgressService.cs b/GermanApp/Application/Services/ProgressService.cs new file mode 100644 index 0000000..b2bb4a0 --- /dev/null +++ b/GermanApp/Application/Services/ProgressService.cs @@ -0,0 +1,206 @@ +using GermanApp.Application.DTOs; +using GermanApp.Domain.Entities; +using GermanApp.Domain.Interfaces; + +namespace GermanApp.Application.Services; + +/// +/// Application service for managing user progress through lessons. +/// This is part of the Application layer. +/// +public class ProgressService +{ + private readonly IUserProgressRepository _userProgressRepository; + private readonly ILessonRepository _lessonRepository; + private readonly ILevelRepository _levelRepository; + + public ProgressService( + IUserProgressRepository userProgressRepository, + ILessonRepository lessonRepository, + ILevelRepository levelRepository) + { + _userProgressRepository = userProgressRepository; + _lessonRepository = lessonRepository; + _levelRepository = levelRepository; + } + + /// + /// Gets user progress for a specific lesson. + /// + public async Task GetUserProgressAsync(int userId, int lessonId, CancellationToken cancellationToken = default) + { + var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, lessonId, cancellationToken); + return progress?.ToDto(); + } + + /// + /// Gets all progress records for a user. + /// + public async Task> GetUserProgressAsync(int userId, CancellationToken cancellationToken = default) + { + var progressRecords = await _userProgressRepository.GetByUserAsync(userId, cancellationToken); + return progressRecords.Select(p => p.ToDto()).ToList(); + } + + /// + /// Gets progress for all lessons in a specific level for a user. + /// + public async Task> GetUserProgressByLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default) + { + var progressRecords = await _userProgressRepository.GetByUserAndLevelAsync(userId, levelId, cancellationToken); + return progressRecords.Select(p => p.ToDto()).ToList(); + } + + /// + /// Checks if a user has completed a lesson. + /// + public async Task HasUserCompletedLessonAsync(int userId, int lessonId, CancellationToken cancellationToken = default) + { + return await _userProgressRepository.HasUserCompletedLessonAsync(userId, lessonId, cancellationToken); + } + + /// + /// Marks a lesson as completed for a user with a quiz score. + /// + public async Task MarkLessonAsCompletedAsync(int userId, int lessonId, int quizScore, CancellationToken cancellationToken = default) + { + var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, lessonId, cancellationToken); + + if (progress == null) + { + // Create new progress record + progress = UserProgress.Create(userId, lessonId); + progress.MarkAsCompleted(quizScore); + await _userProgressRepository.AddAsync(progress, cancellationToken); + } + else + { + // Update existing progress record + progress.MarkAsCompleted(quizScore); + await _userProgressRepository.UpdateAsync(progress, cancellationToken); + } + + return progress.ToDto(); + } + + /// + /// Updates user progress for a lesson without marking as completed. + /// + public async Task UpdateUserProgressAsync(int userId, UpdateUserProgressDto dto, CancellationToken cancellationToken = default) + { + var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, dto.LessonId, cancellationToken); + + if (progress == null) + { + // Create new progress record + progress = dto.ToEntity(userId); + await _userProgressRepository.AddAsync(progress, cancellationToken); + } + else + { + // Update existing progress record + progress.UpdateFromDto(dto); + await _userProgressRepository.UpdateAsync(progress, cancellationToken); + } + + return progress.ToDto(); + } + + /// + /// Resets user progress for a lesson. + /// + public async Task ResetLessonProgressAsync(int userId, int lessonId, CancellationToken cancellationToken = default) + { + var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, lessonId, cancellationToken); + if (progress == null) + return false; + + progress.Reset(); + await _userProgressRepository.UpdateAsync(progress, cancellationToken); + return true; + } + + /// + /// Gets the user's average score for a level. + /// + public async Task GetAverageScoreForLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default) + { + return await _userProgressRepository.GetAverageScoreForLevelAsync(userId, levelId, cancellationToken); + } + + /// + /// Gets the percentage of lessons completed in a level. + /// + public async Task GetLevelCompletionPercentageAsync(int userId, int levelId, CancellationToken cancellationToken = default) + { + return await _userProgressRepository.GetLevelCompletionPercentageAsync(userId, levelId, cancellationToken); + } + + /// + /// Gets level completion summaries for a user. + /// + public async Task> GetLevelCompletionsAsync(int userId, CancellationToken cancellationToken = default) + { + var levels = await _levelRepository.GetAllOrderedAsync(cancellationToken); + var completions = new List(); + + foreach (var level in levels) + { + var lessons = await _lessonRepository.GetByLevelAsync(level.Id, cancellationToken); + var completedCount = 0; + + foreach (var lesson in lessons) + { + var hasCompleted = await _userProgressRepository.HasUserCompletedLessonAsync(userId, lesson.Id, cancellationToken); + if (hasCompleted) + completedCount++; + } + + var percentage = lessons.Count > 0 + ? (double)completedCount / lessons.Count * 100 + : 0; + + completions.Add(new LevelCompletionDto( + level.Id, + level.Name, + level.Code, + lessons.Count, + completedCount, + percentage + )); + } + + return completions; + } + + /// + /// Gets lessons that the user can access (completed previous lessons or first in level). + /// + public async Task> GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default) + { + var lessons = await _lessonRepository.GetAccessibleLessonsAsync(userId, cancellationToken); + return lessons.Select(l => l.ToDto()).ToList(); + } + + /// + /// Checks if the next lesson in a level is unlocked for the user. + /// + public async Task 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; + } +} diff --git a/GermanApp/Program.cs b/GermanApp/Program.cs index 978762f..dab9913 100644 --- a/GermanApp/Program.cs +++ b/GermanApp/Program.cs @@ -1,4 +1,5 @@ using GermanApp.Application.DTOs; +using GermanApp.Application.Services; using GermanApp.Application.Interfaces; using GermanApp.Application.UseCases.Commands; using GermanApp.Domain.Entities; @@ -115,13 +116,20 @@ try }); // Register repositories (Infrastructure implementations of Domain interfaces) + builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); // ============================================ // APPLICATION LAYER - Use Cases & Services // ============================================ // Register command handlers + + // Register application services + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped, CreateLessonCommandHandler>(); var app = builder.Build();