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();