using System.Data; using Dapper; using PlotLine.Models; namespace PlotLine.Data; public interface IStoryIntelligenceResultRepository { Task SaveAsync(StoryIntelligenceRunSaveRequest request); Task> ListRunsAsync(); Task GetRunAsync(int runId); Task GetChapterResultAsync(int runId); Task> ListSceneResultsAsync(int runId); } public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligenceResultRepository { public async Task SaveAsync(StoryIntelligenceRunSaveRequest request) { using var connection = connectionFactory.CreateConnection(); connection.Open(); using var transaction = connection.BeginTransaction(); try { var runId = await connection.QuerySingleAsync( "dbo.StoryIntelligenceRun_Save", new { request.UserID, request.ProjectID, request.BookID, request.Status, request.SourceType, request.SourceFileName, request.SourceFileSizeBytes, request.SourceWordCount, request.SourceCharacterCount, request.SourceChapterCount, request.SourceDetectedImagesCount, request.SourceDetectedTablesCount, request.SourceDetectedFootnotesCount, request.SourceDetectedCommentsCount, request.PromptVersion, request.Model, request.StartedUtc, request.CompletedUtc, request.FailureStage, request.TotalInputTokens, request.TotalOutputTokens, request.TotalTokens, request.TotalDurationMs, request.EstimatedCostGBP, request.EstimatedCostUSD, request.PromptVersionsSummary, request.ErrorMessage, request.ErrorDetail }, transaction, commandType: CommandType.StoredProcedure); int? chapterResultId = null; if (request.ChapterResult is not null) { var chapter = request.ChapterResult; chapterResultId = await connection.QuerySingleAsync( "dbo.StoryIntelligenceChapterResult_Save", new { StoryIntelligenceRunID = runId, chapter.ProjectID, chapter.BookID, chapter.ChapterID, chapter.ChapterNumber, chapter.SourceLabel, chapter.PromptVersion, chapter.Model, chapter.RawResponseJson, chapter.OutputTextJson, chapter.ParsedJson, chapter.ValidationErrorsCount, chapter.ValidationWarningsCount, chapter.InputTokens, chapter.OutputTokens, chapter.TotalTokens, chapter.DurationMs }, transaction, commandType: CommandType.StoredProcedure); } foreach (var scene in request.SceneResults) { await connection.ExecuteAsync( "dbo.StoryIntelligenceSceneResult_Save", new { StoryIntelligenceRunID = runId, ChapterResultID = chapterResultId, scene.ProjectID, scene.BookID, scene.ChapterID, scene.SceneID, scene.TemporarySceneNumber, scene.StartParagraph, scene.EndParagraph, scene.SourceLabel, scene.PromptVersion, scene.Model, scene.RawResponseJson, scene.OutputTextJson, scene.ParsedJson, scene.ValidationErrorsCount, scene.ValidationWarningsCount, scene.InputTokens, scene.OutputTokens, scene.TotalTokens, scene.DurationMs }, transaction, commandType: CommandType.StoredProcedure); } transaction.Commit(); return runId; } catch { transaction.Rollback(); throw; } } public async Task> ListRunsAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.StoryIntelligenceRun_ListAdmin", commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetRunAsync(int runId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.StoryIntelligenceRun_GetAdmin", new { StoryIntelligenceRunID = runId }, commandType: CommandType.StoredProcedure); } public async Task GetChapterResultAsync(int runId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.StoryIntelligenceChapterResult_GetByRun", new { StoryIntelligenceRunID = runId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneResultsAsync(int runId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.StoryIntelligenceSceneResult_ListByRun", new { StoryIntelligenceRunID = runId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } }