DeutschLernen/GermanApp/Application/Services/StoryUnlockService.cs
Lasse Rune Hansen 6693165f83 feat(backend/story-integration): Phase 2 - Backend Services
Implement story integration backend services:
- Create StoryService (Application/Services/StoryService.cs) with full CRUD operations
- Create StoryGenerationService (Application/Services/StoryGenerationService.cs) for AI-powered story generation
  - Uses IMistralService for text generation
  - Uses ITtsService for audio generation
  - Splits stories into segments per lesson
- Create StoryUnlockService (Application/Services/StoryUnlockService.cs) for progress management
  - Handles lesson completion → story segment unlocking
- Create StoryController (Presentation/Controllers/StoryController.cs) with 12 endpoints:
  - GET /api/story/levels - list levels with stories
  - GET /api/story/levels/{levelId}/segments - get segments for level
  - GET /api/story/segments/{id} - get specific segment
  - POST /api/story/segments - create segment (Admin)
  - PUT /api/story/segments/{id} - update segment (Admin)
  - DELETE /api/story/segments/{id} - delete segment (Admin)
  - POST /api/story/levels/{levelId}/generate - generate story with AI (Admin)
  - POST /api/story/segments/{segmentId}/audio - generate audio (Admin)
  - GET /api/story/levels/{levelId}/progress - get user progress
  - POST /api/story/segments/{segmentId}/complete - mark as completed
  - GET /api/story/levels/{levelId}/next - get next segment
  - GET /api/story/segments/{segmentId}/unlocked - check if unlocked
  - GET /api/story/segments/{segmentId}/audio - get audio URL
  - GET /api/story/levels/{levelId}/lessons-with-stories - get lessons with story status
- Register all services in Program.cs DI container
- Update feature document to reflect Phase 2 completion

Next: Phase 3 - AI Integration (unit tests for services)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-13 13:37:16 +02:00

183 lines
6.7 KiB
C#

using System.Threading;
using System.Threading.Tasks;
using GermanApp.Application.DTOs;
using GermanApp.Domain.Entities;
using GermanApp.Domain.Interfaces;
using Microsoft.Extensions.Logging;
namespace GermanApp.Application.Services;
/// <summary>
/// Application service for managing story segment unlocking.
/// This service is called when a user completes a lesson to unlock the next story segment.
/// This is part of the Application layer.
/// </summary>
public class StoryUnlockService
{
private readonly StoryService _storyService;
private readonly IUserProgressRepository _userProgressRepository;
private readonly ILessonRepository _lessonRepository;
private readonly ILogger<StoryUnlockService> _logger;
/// <summary>
/// Creates a new StoryUnlockService.
/// </summary>
/// <param name="storyService">Service for story segment operations</param>
/// <param name="userProgressRepository">Repository for user progress</param>
/// <param name="lessonRepository">Repository for lessons</param>
/// <param name="logger">Logger for service operations</param>
public StoryUnlockService(
StoryService storyService,
IUserProgressRepository userProgressRepository,
ILessonRepository lessonRepository,
ILogger<StoryUnlockService> logger)
{
_storyService = storyService;
_userProgressRepository = userProgressRepository;
_lessonRepository = lessonRepository;
_logger = logger;
}
/// <summary>
/// Called when a user completes a lesson.
/// Checks if the user should unlock a new story segment and does so.
/// </summary>
/// <param name="userId">The user ID</param>
/// <param name="lessonId">The lesson ID that was completed</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if a new segment was unlocked, false otherwise</returns>
public virtual async Task<bool> HandleLessonCompletionAsync(
int userId,
int lessonId,
CancellationToken cancellationToken = default)
{
_logger.LogInformation(
"Handling lesson completion for user {UserId}, lesson {LessonId}",
userId, lessonId);
// Get the lesson to find its level and order
var lesson = await _lessonRepository.GetByIdAsync(lessonId, cancellationToken);
if (lesson == null)
{
_logger.LogWarning("Lesson not found: {LessonId}", lessonId);
return false;
}
// Check if this lesson completion qualifies for unlocking a story segment
// We need to check if the user has actually completed this lesson
var isCompleted = await _userProgressRepository.HasUserCompletedLessonAsync(
userId, lessonId, cancellationToken);
if (!isCompleted)
{
_logger.LogInformation(
"Lesson {LessonId} not marked as completed for user {UserId}",
lessonId, userId);
return false;
}
// Get the next story segment to unlock
var nextSegment = await _storyService.GetNextSegmentToUnlockAsync(
lesson.LevelId,
lesson.Order,
cancellationToken);
if (nextSegment == null)
{
_logger.LogInformation(
"No story segment to unlock for user {UserId} after lesson {LessonId}",
userId, lessonId);
return false;
}
// Check if user already has this segment unlocked
var alreadyUnlocked = await _storyService.IsSegmentUnlockedAsync(
userId, nextSegment.Id, cancellationToken);
if (alreadyUnlocked)
{
_logger.LogInformation(
"Segment {SegmentId} already unlocked for user {UserId}",
nextSegment.Id, userId);
return false;
}
// Unlock the segment
var unlockedSegment = await _storyService.UnlockNextSegmentAsync(
userId,
lesson.LevelId,
lesson.Order,
cancellationToken);
if (unlockedSegment != null)
{
_logger.LogInformation(
"Unlocked story segment {SegmentId} for user {UserId} after completing lesson {LessonId}",
unlockedSegment.Id, userId, lessonId);
return true;
}
return false;
}
/// <summary>
/// Checks if a user has unlocked a specific story segment.
/// </summary>
/// <param name="userId">The user ID</param>
/// <param name="segmentId">The segment ID</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if the segment is unlocked</returns>
public virtual async Task<bool> IsSegmentUnlockedAsync(
int userId,
int segmentId,
CancellationToken cancellationToken = default)
{
return await _storyService.IsSegmentUnlockedAsync(userId, segmentId, cancellationToken);
}
/// <summary>
/// Checks if a user has completed (read/listened to) a specific story segment.
/// </summary>
/// <param name="userId">The user ID</param>
/// <param name="segmentId">The segment ID</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if the segment is completed</returns>
public virtual async Task<bool> IsSegmentCompletedAsync(
int userId,
int segmentId,
CancellationToken cancellationToken = default)
{
return await _storyService.IsSegmentCompletedAsync(userId, segmentId, cancellationToken);
}
/// <summary>
/// Marks a story segment as completed for a user.
/// </summary>
/// <param name="userId">The user ID</param>
/// <param name="segmentId">The segment ID</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if the segment was marked as completed</returns>
public virtual async Task<bool> MarkSegmentAsCompletedAsync(
int userId,
int segmentId,
CancellationToken cancellationToken = default)
{
return await _storyService.MarkSegmentAsCompletedAsync(userId, segmentId, cancellationToken);
}
/// <summary>
/// Gets a user's story progress for a level.
/// </summary>
/// <param name="userId">The user ID</param>
/// <param name="levelId">The level ID</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Story progress DTO</returns>
public virtual async Task<StoryProgressDto> GetUserProgressAsync(
int userId,
int levelId,
CancellationToken cancellationToken = default)
{
return await _storyService.GetUserProgressAsync(userId, levelId, cancellationToken);
}
}