From 7c9e9b0b6f31de0772582dc12d4975a60e8b8823 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sat, 13 Jun 2026 20:03:30 +0100 Subject: [PATCH] Phase 10E.2: Fix Word Companion Resolve Scene Matching --- .../Controllers/WordCompanionController.cs | 8 +- PlotLine/Models/WordCompanionApiModels.cs | 7 +- ...10E2_WordCompanionResolveSceneMatching.sql | 90 +++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 PlotLine/Sql/058_Phase10E2_WordCompanionResolveSceneMatching.sql diff --git a/PlotLine/Controllers/WordCompanionController.cs b/PlotLine/Controllers/WordCompanionController.cs index a7358f3..ba59d5e 100644 --- a/PlotLine/Controllers/WordCompanionController.cs +++ b/PlotLine/Controllers/WordCompanionController.cs @@ -31,7 +31,12 @@ public sealed class WordCompanionController(IWordCompanionService wordCompanion) public async Task ResolveScene(int bookId, WordCompanionResolveSceneRequest request) { var response = await wordCompanion.ResolveSceneAsync(bookId, request); - return response is null ? BadRequest() : Ok(response); + if (response is null) + { + return BadRequest(); + } + + return response.BookAccessible ? Ok(response) : NotFound(response); } [HttpGet("scenes/{sceneId:int}/companion")] @@ -69,4 +74,3 @@ public sealed class WordCompanionController(IWordCompanionService wordCompanion) return response is null ? BadRequest() : Ok(response); } } - diff --git a/PlotLine/Models/WordCompanionApiModels.cs b/PlotLine/Models/WordCompanionApiModels.cs index 2878278..965d0be 100644 --- a/PlotLine/Models/WordCompanionApiModels.cs +++ b/PlotLine/Models/WordCompanionApiModels.cs @@ -59,6 +59,12 @@ public sealed class WordCompanionResolveSceneResponse public int? ChapterId { get; init; } public int? SceneId { get; init; } public string? MatchType { get; init; } + public int? BookId { get; init; } + public string? ChapterTitleSearched { get; init; } + public string? SceneTitleSearched { get; init; } + public int? AvailableChapterCount { get; init; } + public int? AvailableSceneCount { get; init; } + public bool BookAccessible { get; init; } = true; } public sealed class WordCompanionLinkItemDto @@ -166,4 +172,3 @@ public sealed class WordCompanionUpdateWordCountResponse public int ActualWords { get; init; } public string Message { get; init; } = "Word count updated."; } - diff --git a/PlotLine/Sql/058_Phase10E2_WordCompanionResolveSceneMatching.sql b/PlotLine/Sql/058_Phase10E2_WordCompanionResolveSceneMatching.sql new file mode 100644 index 0000000..2ba6188 --- /dev/null +++ b/PlotLine/Sql/058_Phase10E2_WordCompanionResolveSceneMatching.sql @@ -0,0 +1,90 @@ +CREATE OR ALTER PROCEDURE dbo.WordCompanion_Scene_Resolve + @BookID int, + @UserID int, + @ChapterTitle nvarchar(200), + @SceneTitle nvarchar(200) +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @ChapterTitleSearched nvarchar(200) = + LTRIM(RTRIM(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(@ChapterTitle, N''), NCHAR(160), N' '), CHAR(9), N' '), CHAR(13), N' '), CHAR(10), N' '))); + DECLARE @SceneTitleSearched nvarchar(200) = + LTRIM(RTRIM(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(@SceneTitle, N''), NCHAR(160), N' '), CHAR(9), N' '), CHAR(13), N' '), CHAR(10), N' '))); + + IF NOT EXISTS + ( + SELECT 1 + FROM dbo.Books b + INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID + INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID + WHERE b.BookID = @BookID + AND b.IsArchived = 0 + AND p.IsArchived = 0 + AND pua.UserID = @UserID + AND pua.IsActive = 1 + ) + BEGIN + SELECT CAST(0 AS bit) AS Matched, + CAST(NULL AS int) AS ChapterId, + CAST(NULL AS int) AS SceneId, + CAST(NULL AS nvarchar(50)) AS MatchType, + @BookID AS BookId, + @ChapterTitleSearched AS ChapterTitleSearched, + @SceneTitleSearched AS SceneTitleSearched, + CAST(0 AS int) AS AvailableChapterCount, + CAST(0 AS int) AS AvailableSceneCount, + CAST(0 AS bit) AS BookAccessible; + RETURN; + END; + + DECLARE @AvailableChapterCount int; + DECLARE @AvailableSceneCount int; + + SELECT @AvailableChapterCount = COUNT(*) + FROM dbo.Chapters c + WHERE c.BookID = @BookID + AND c.IsArchived = 0; + + SELECT @AvailableSceneCount = COUNT(*) + FROM dbo.Chapters c + INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID + WHERE c.BookID = @BookID + AND c.IsArchived = 0 + AND s.IsArchived = 0; + + DECLARE @ChapterID int; + DECLARE @SceneID int; + + SELECT TOP (1) + @ChapterID = c.ChapterID, + @SceneID = s.SceneID + FROM dbo.Chapters c + INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID + CROSS APPLY + ( + SELECT LTRIM(RTRIM(REPLACE(REPLACE(REPLACE(REPLACE(c.ChapterTitle, NCHAR(160), N' '), CHAR(9), N' '), CHAR(13), N' '), CHAR(10), N' '))) AS NormalizedChapterTitle + ) chapterMatch + CROSS APPLY + ( + SELECT LTRIM(RTRIM(REPLACE(REPLACE(REPLACE(REPLACE(s.SceneTitle, NCHAR(160), N' '), CHAR(9), N' '), CHAR(13), N' '), CHAR(10), N' '))) AS NormalizedSceneTitle + ) sceneMatch + WHERE c.BookID = @BookID + AND c.IsArchived = 0 + AND s.IsArchived = 0 + AND chapterMatch.NormalizedChapterTitle COLLATE SQL_Latin1_General_CP1_CI_AS = @ChapterTitleSearched COLLATE SQL_Latin1_General_CP1_CI_AS + AND sceneMatch.NormalizedSceneTitle COLLATE SQL_Latin1_General_CP1_CI_AS = @SceneTitleSearched COLLATE SQL_Latin1_General_CP1_CI_AS + ORDER BY c.SortOrder, s.SortOrder, s.SceneID; + + SELECT CAST(CASE WHEN @SceneID IS NULL THEN 0 ELSE 1 END AS bit) AS Matched, + @ChapterID AS ChapterId, + @SceneID AS SceneId, + CASE WHEN @SceneID IS NULL THEN CAST(NULL AS nvarchar(50)) ELSE N'ExactTitle' END AS MatchType, + @BookID AS BookId, + @ChapterTitleSearched AS ChapterTitleSearched, + @SceneTitleSearched AS SceneTitleSearched, + @AvailableChapterCount AS AvailableChapterCount, + @AvailableSceneCount AS AvailableSceneCount, + CAST(1 AS bit) AS BookAccessible; +END; +GO