refactor(api): extract ProfileCv DTOs, add missing AsNoTracking on reads

This commit is contained in:
cesnimda
2026-07-12 20:12:36 +02:00
parent ea6c3650f3
commit 4cfdc95b59
3 changed files with 36 additions and 32 deletions
+14 -14
View File
@@ -123,7 +123,7 @@ public sealed class ProfileCvControllerTests
var result = await controller.GetRuns();
var ok = Assert.IsType<OkObjectResult>(result.Result);
var runs = Assert.IsAssignableFrom<IEnumerable<ProfileCvController.CvExtractionRunListItem>>(ok.Value);
var runs = Assert.IsAssignableFrom<IEnumerable<CvExtractionRunListItem>>(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<ObjectResult>(result);
Assert.Equal(StatusCodes.Status502BadGateway, objectResult.StatusCode);
var payload = Assert.IsType<ProfileCvController.CvRewriteFailureDto>(objectResult.Value);
var payload = Assert.IsType<CvRewriteFailureDto>(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<ObjectResult>(result);
Assert.Equal(StatusCodes.Status502BadGateway, objectResult.StatusCode);
var payload = Assert.IsType<ProfileCvController.CvRewriteFailureDto>(objectResult.Value);
var payload = Assert.IsType<CvRewriteFailureDto>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(result.Result);
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
@@ -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<string> 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))
{
@@ -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<string> 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);