- StoryServiceTests.cs: 536 lines, comprehensive tests for StoryService - StoryGenerationServiceTests.cs: 469 lines, tests for AI story generation - StoryUnlockServiceTests.cs: 292 lines, tests for story unlock logic - StoryRepositoryTests.cs: 120 lines, tests for EF Core repository - StoryProgressRepositoryTests.cs: 74 lines, tests for progress repository - AppDbContext.cs: Made DbSet properties virtual for Moq compatibility - Fixed return types in StoryRepository and StoryProgressRepository - Updated docs/features/story-integration.md for Phase 3 completion - All 324 unit tests pass Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
153 lines
5.1 KiB
C#
153 lines
5.1 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using GermanApp.Domain.Entities;
|
|
using GermanApp.Domain.Interfaces;
|
|
using GermanApp.Infrastructure.Data.DbContext;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace GermanApp.Infrastructure.Data.Repositories;
|
|
|
|
/// <summary>
|
|
/// Entity Framework Core implementation of IStoryRepository.
|
|
/// This is part of the Infrastructure layer.
|
|
/// </summary>
|
|
public class StoryRepository : IStoryRepository
|
|
{
|
|
private readonly AppDbContext _context;
|
|
|
|
public StoryRepository(AppDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
public async Task<StorySegment?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.StorySegments
|
|
.Include(s => s.Level)
|
|
.Include(s => s.Lesson)
|
|
.FirstOrDefaultAsync(s => s.Id == id, cancellationToken);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<StorySegment>> GetByLevelAsync(
|
|
int levelId,
|
|
bool includeInactive = false,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = _context.StorySegments
|
|
.Where(s => s.LevelId == levelId);
|
|
|
|
if (!includeInactive)
|
|
{
|
|
query = query.Where(s => s.IsActive);
|
|
}
|
|
|
|
return await query
|
|
.Include(s => s.Level)
|
|
.Include(s => s.Lesson)
|
|
.OrderBy(s => s.Order)
|
|
.AsNoTracking()
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<StorySegment>> GetByLessonAsync(
|
|
int lessonId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.StorySegments
|
|
.Where(s => s.LessonId == lessonId)
|
|
.Include(s => s.Level)
|
|
.Include(s => s.Lesson)
|
|
.OrderBy(s => s.Order)
|
|
.AsNoTracking()
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<StorySegment?> GetNextSegmentToUnlockAsync(
|
|
int levelId,
|
|
int completedLessonOrder,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
// Get the next lesson order (the one after the completed lesson)
|
|
// Then find the story segment with matching Lesson.Order
|
|
var nextLessonOrder = completedLessonOrder + 1;
|
|
|
|
return await _context.StorySegments
|
|
.Where(s => s.LevelId == levelId && s.LessonId != null && s.IsActive)
|
|
.Where(s => s.Lesson != null && s.Lesson.Order == nextLessonOrder)
|
|
.Include(s => s.Lesson)
|
|
.OrderBy(s => s.Order)
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<StorySegment>> GetByOrderRangeAsync(
|
|
int levelId,
|
|
int startOrder,
|
|
int endOrder,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.StorySegments
|
|
.Where(s => s.LevelId == levelId
|
|
&& s.Order >= startOrder
|
|
&& s.Order <= endOrder
|
|
&& s.IsActive)
|
|
.Include(s => s.Level)
|
|
.Include(s => s.Lesson)
|
|
.OrderBy(s => s.Order)
|
|
.AsNoTracking()
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<StorySegment> AddAsync(StorySegment segment, CancellationToken cancellationToken = default)
|
|
{
|
|
await _context.StorySegments.AddAsync(segment, cancellationToken);
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
return segment;
|
|
}
|
|
|
|
public async Task UpdateAsync(StorySegment segment, CancellationToken cancellationToken = default)
|
|
{
|
|
_context.StorySegments.Update(segment);
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task DeleteAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
var segment = await _context.StorySegments.FindAsync(new object[] { id }, cancellationToken);
|
|
if (segment != null)
|
|
{
|
|
_context.StorySegments.Remove(segment);
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
}
|
|
}
|
|
|
|
public async Task<int> GetMaxOrderAsync(int levelId, CancellationToken cancellationToken = default)
|
|
{
|
|
var maxOrder = await _context.StorySegments
|
|
.Where(s => s.LevelId == levelId)
|
|
.Select(s => s.Order)
|
|
.MaxAsync(cancellationToken);
|
|
|
|
return maxOrder;
|
|
}
|
|
|
|
public async Task<bool> ExistsAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.StorySegments
|
|
.AnyAsync(s => s.Id == id, cancellationToken);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<StorySegment>> GetSegmentsNeedingAudioAsync(
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.StorySegments
|
|
.Where(s => string.IsNullOrEmpty(s.AudioUrl) && s.IsActive)
|
|
.Include(s => s.Level)
|
|
.Include(s => s.Lesson)
|
|
.OrderBy(s => s.LevelId)
|
|
.ThenBy(s => s.Order)
|
|
.AsNoTracking()
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
}
|