From 4cfdc95b590a6eb929c98cccb5e8c898b4ebbc74 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sun, 12 Jul 2026 20:12:36 +0200 Subject: [PATCH] refactor(api): extract ProfileCv DTOs, add missing AsNoTracking on reads --- .../ProfileCvControllerTests.cs | 28 +++++++++---------- .../Controllers/ProfileCvController.cs | 20 ++----------- JobTrackerApi/Controllers/ProfileCvDtos.cs | 20 +++++++++++++ 3 files changed, 36 insertions(+), 32 deletions(-) create mode 100644 JobTrackerApi/Controllers/ProfileCvDtos.cs diff --git a/JobTrackerApi.Tests/ProfileCvControllerTests.cs b/JobTrackerApi.Tests/ProfileCvControllerTests.cs index cee20a0..e41a93c 100644 --- a/JobTrackerApi.Tests/ProfileCvControllerTests.cs +++ b/JobTrackerApi.Tests/ProfileCvControllerTests.cs @@ -123,7 +123,7 @@ public sealed class ProfileCvControllerTests var result = await controller.GetRuns(); var ok = Assert.IsType(result.Result); - var runs = Assert.IsAssignableFrom>(ok.Value); + var runs = Assert.IsAssignableFrom>(ok.Value); var single = Assert.Single(runs); Assert.Equal("upload", single.Trigger); Assert.Equal("applied", single.Status); @@ -611,7 +611,7 @@ public sealed class ProfileCvControllerTests var objectResult = Assert.IsType(result); Assert.Equal(StatusCodes.Status502BadGateway, objectResult.StatusCode); - var payload = Assert.IsType(objectResult.Value); + var payload = Assert.IsType(objectResult.Value); Assert.Equal("ai-service-unavailable", payload.Code); Assert.Contains("could not rewrite", payload.Message, StringComparison.OrdinalIgnoreCase); Assert.Contains("unavailable", payload.Detail ?? string.Empty, StringComparison.OrdinalIgnoreCase); @@ -673,7 +673,7 @@ public sealed class ProfileCvControllerTests var objectResult = Assert.IsType(result); Assert.Equal(StatusCodes.Status502BadGateway, objectResult.StatusCode); - var payload = Assert.IsType(objectResult.Value); + var payload = Assert.IsType(objectResult.Value); Assert.Equal("rewrite-empty", payload.Code); Assert.Contains("empty", payload.Message, StringComparison.OrdinalIgnoreCase); Assert.Contains("no usable text", payload.Detail ?? string.Empty, StringComparison.OrdinalIgnoreCase); @@ -766,7 +766,7 @@ public sealed class ProfileCvControllerTests var paths = CreatePaths(); var controller = CreateController(userManager.Object, aiService.Object, db, paths); - var result = await controller.Parse(new ProfileCvController.ParseCvRequest(user.ProfileCvText)); + var result = await controller.Parse(new ParseCvRequest(user.ProfileCvText)); var ok = Assert.IsType(result.Result); var json = JsonSerializer.Serialize(ok.Value); @@ -800,7 +800,7 @@ public sealed class ProfileCvControllerTests var paths = CreatePaths(); var controller = CreateController(userManager.Object, aiService.Object, db, paths); - var result = await controller.Parse(new ProfileCvController.ParseCvRequest(user.ProfileCvText)); + var result = await controller.Parse(new ParseCvRequest(user.ProfileCvText)); var ok = Assert.IsType(result.Result); var json = JsonSerializer.Serialize(ok.Value); @@ -838,7 +838,7 @@ public sealed class ProfileCvControllerTests var paths = CreatePaths(); var controller = CreateController(userManager.Object, aiService.Object, db, paths, classifier.Object); - var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source)); + var result = await controller.Parse(new ParseCvRequest(source)); var ok = Assert.IsType(result.Result); var json = JsonSerializer.Serialize(ok.Value); @@ -878,7 +878,7 @@ public sealed class ProfileCvControllerTests var paths = CreatePaths(); var controller = CreateController(userManager.Object, aiService.Object, db, paths, classifier.Object); - var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source)); + var result = await controller.Parse(new ParseCvRequest(source)); var ok = Assert.IsType(result.Result); var json = JsonSerializer.Serialize(ok.Value); @@ -914,7 +914,7 @@ public sealed class ProfileCvControllerTests var paths = CreatePaths(); var controller = CreateController(userManager.Object, aiService.Object, db, paths, classifier.Object); - var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source)); + var result = await controller.Parse(new ParseCvRequest(source)); var ok = Assert.IsType(result.Result); var json = JsonSerializer.Serialize(ok.Value); @@ -1030,7 +1030,7 @@ public sealed class ProfileCvControllerTests await using var db = CreateDb(); var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths(), null, normalizer.Object); - var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source)); + var result = await controller.Parse(new ParseCvRequest(source)); Assert.IsType(result.Result); var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson); @@ -1069,7 +1069,7 @@ public sealed class ProfileCvControllerTests var paths = CreatePaths(); var controller = CreateController(userManager.Object, aiService.Object, db, paths); - var result = await controller.Parse(new ProfileCvController.ParseCvRequest(rawSource)); + var result = await controller.Parse(new ParseCvRequest(rawSource)); var ok = Assert.IsType(result.Result); Assert.NotNull(ok.Value); @@ -1098,7 +1098,7 @@ public sealed class ProfileCvControllerTests await using var db = CreateDb(); var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths()); - var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source)); + var result = await controller.Parse(new ParseCvRequest(source)); Assert.IsType(result.Result); var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson); @@ -1129,7 +1129,7 @@ public sealed class ProfileCvControllerTests await using var db = CreateDb(); var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths()); - var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source)); + var result = await controller.Parse(new ParseCvRequest(source)); Assert.IsType(result.Result); var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson); @@ -1158,7 +1158,7 @@ public sealed class ProfileCvControllerTests await using var db = CreateDb(); var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths()); - var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source)); + var result = await controller.Parse(new ParseCvRequest(source)); Assert.IsType(result.Result); var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson); @@ -1186,7 +1186,7 @@ public sealed class ProfileCvControllerTests await using var db = CreateDb(); var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths()); - var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source)); + var result = await controller.Parse(new ParseCvRequest(source)); Assert.IsType(result.Result); var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson); diff --git a/JobTrackerApi/Controllers/ProfileCvController.cs b/JobTrackerApi/Controllers/ProfileCvController.cs index a599d69..8f87075 100644 --- a/JobTrackerApi/Controllers/ProfileCvController.cs +++ b/JobTrackerApi/Controllers/ProfileCvController.cs @@ -113,25 +113,8 @@ public sealed class ProfileCvController : ControllerBase public string? Tone { get; set; } public string? Language { get; set; } } - public sealed record ParseCvRequest(string? Text); - public sealed record CvTemplateDescriptor(string Id, string Title, string Tone, string AccentColor, string PreviewTagline, string PreviewSummary, List PreviewBullets); - public sealed record ProfileCvPreviewDto(string TemplateId, string Html, string SuggestedFileName, string FullText, string RewrittenText, string? SectionName, StructuredCvProfile StructuredCv, TailoredCvDocument Document, string? TargetRole, int? JobApplicationId); - public sealed record CvRewriteFailureDto(string Code, string Message, string? Detail = null, string? LastAiError = null); - private sealed record ExtractionPipelineResult(string RawText, string NormalizedText, StructuredCvProfile StructuredCv); private sealed record ClassifiedCvBlock(int Index, string OriginalBlock, string SectionName, string Content, CvBlockClassificationResult? Classification); - public sealed record CvExtractionRunListItem( - int Id, - string Trigger, - string Status, - string? ArtifactFileName, - DateTimeOffset StartedAtUtc, - DateTimeOffset? CompletedAtUtc, - DateTimeOffset? AppliedAtUtc, - string ParserVersion, - string NormalizerVersion, - string LlmPromptVersion, - string? ErrorMessage); [HttpPost("upload")] [RequestSizeLimit(MaxFileSizeBytes)] @@ -254,6 +237,7 @@ public sealed class ProfileCvController : ControllerBase if (user is null) return Unauthorized(); var artifact = await _db.CvUploadArtifacts + .AsNoTracking() .OrderByDescending(x => x.UploadedAtUtc) .FirstOrDefaultAsync(x => x.OwnerUserId == user.Id, HttpContext.RequestAborted); @@ -941,7 +925,7 @@ public sealed class ProfileCvController : ControllerBase } case "reprocess": { - var artifact = await _db.CvUploadArtifacts.FirstOrDefaultAsync(x => x.Id == run.ArtifactId && x.OwnerUserId == user.Id, cancellationToken); + var artifact = await _db.CvUploadArtifacts.AsNoTracking().FirstOrDefaultAsync(x => x.Id == run.ArtifactId && x.OwnerUserId == user.Id, cancellationToken); if (artifact is null) throw new InvalidOperationException("Upload a CV before reprocessing it."); if (string.IsNullOrWhiteSpace(artifact.StoragePath) || !System.IO.File.Exists(artifact.StoragePath)) { diff --git a/JobTrackerApi/Controllers/ProfileCvDtos.cs b/JobTrackerApi/Controllers/ProfileCvDtos.cs new file mode 100644 index 0000000..a8e56b8 --- /dev/null +++ b/JobTrackerApi/Controllers/ProfileCvDtos.cs @@ -0,0 +1,20 @@ +using JobTrackerApi.Models; + +namespace JobTrackerApi.Controllers; + +public sealed record ParseCvRequest(string? Text); +public sealed record CvTemplateDescriptor(string Id, string Title, string Tone, string AccentColor, string PreviewTagline, string PreviewSummary, List PreviewBullets); +public sealed record ProfileCvPreviewDto(string TemplateId, string Html, string SuggestedFileName, string FullText, string RewrittenText, string? SectionName, StructuredCvProfile StructuredCv, TailoredCvDocument Document, string? TargetRole, int? JobApplicationId); +public sealed record CvRewriteFailureDto(string Code, string Message, string? Detail = null, string? LastAiError = null); +public sealed record CvExtractionRunListItem( + int Id, + string Trigger, + string Status, + string? ArtifactFileName, + DateTimeOffset StartedAtUtc, + DateTimeOffset? CompletedAtUtc, + DateTimeOffset? AppliedAtUtc, + string ParserVersion, + string NormalizerVersion, + string LlmPromptVersion, + string? ErrorMessage);