using Microsoft.EntityFrameworkCore; using Novelly.Api.Common; using Novelly.Api.Common.Validation; using Novelly.Api.Data; namespace Novelly.Api.Projects; public class ProjectService( INovelDbContext db, ILogger logger, IModelValidator createValidator, IModelValidator updateValidator) { public async Task> ListAsync(CancellationToken ct = default) { logger.LogInformation("Listing projects"); return await db.Projects .OrderByDescending(p => p.UpdatedAt) .Select(p => new ProjectSummaryResponse( p.Id, p.Title, p.Author, p.Genre, p.Logline, p.TargetWordCount, p.Phase, p.Characters.Count, p.Chapters.Count, p.Chapters.Sum(c => (int?)c.WordCount) ?? 0, p.UpdatedAt)) .ToListAsync(ct); } public async Task GetAsync(Guid id, CancellationToken ct = default) { Guard.Default(id, nameof(id)); logger.LogInformation("Getting project {ProjectId}", id); return await FindAsync(id, ct); } public async Task CreateAsync(CreateProjectRequest request, CancellationToken ct = default) { Guard.Null(request, nameof(request)); createValidator.Validate(request).ThrowIfInvalid(); logger.LogInformation("Creating project {Title}", request.Title); var project = new Project { Title = request.Title, Author = request.Author, Genre = request.Genre, Logline = request.Logline, Synopsis = request.Synopsis, Notes = request.Notes, TargetWordCount = request.TargetWordCount }; db.Projects.Add(project); await db.SaveChangesAsync(ct); return project; } public async Task UpdateAsync(Guid id, UpdateProjectRequest request, CancellationToken ct = default) { Guard.Default(id, nameof(id)); Guard.Null(request, nameof(request)); updateValidator.Validate(request).ThrowIfInvalid(); logger.LogInformation("Updating project {ProjectId}", id); var project = await FindAsync(id, ct); if (project is null) { return null; } project.Title = Patch.Apply(project.Title, request.Title) ?? project.Title; project.Author = Patch.Apply(project.Author, request.Author); project.Genre = Patch.Apply(project.Genre, request.Genre); project.Logline = Patch.Apply(project.Logline, request.Logline); project.Synopsis = Patch.Apply(project.Synopsis, request.Synopsis); project.Notes = Patch.Apply(project.Notes, request.Notes); project.TargetWordCount = request.TargetWordCount ?? project.TargetWordCount; project.Phase = request.Phase ?? project.Phase; project.UpdatedAt = DateTimeOffset.UtcNow; await db.SaveChangesAsync(ct); return project; } public async Task DeleteAsync(Guid id, CancellationToken ct = default) { Guard.Default(id, nameof(id)); logger.LogInformation("Deleting project {ProjectId}", id); var project = await FindAsync(id, ct); if (project is null) { return false; } db.Projects.Remove(project); await db.SaveChangesAsync(ct); return true; } private async Task FindAsync(Guid id, CancellationToken ct) { logger.LogDebug("Finding project {ProjectId}", id); var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == id, ct); if (project is null) { logger.LogInformation("Project {ProjectId} not found", id); } else { logger.LogDebug("Found project {ProjectId}", id); } return project; } }