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>
183 lines
6.7 KiB
C#
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);
|
|
}
|
|
}
|