refactor(api): extract JobApplications DTOs and helpers, fix N+1 aggregation

- Move inline DTOs to JobApplicationDtos.cs, pure static helpers to JobApplicationHelpers.cs
- GetStats aggregates server-side (COUNT/GROUP BY) instead of loading the full table
- Cache RuleSettings via IMemoryCache, keyed per-user (RulesEngine.GetSettings falls back
  to per-user UserRuleSettings overrides, so a single global cache key would leak settings
  across users)
- Add missing AsNoTracking() to read-only GET endpoints (GetAll, GetById, GetBoard,
  GetReminders, GetStatusSuggestion, GetMatchScore, GetCandidateFit, GetFocusPlan,
  GetInterviewPrep, GetReadiness)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-12 20:08:59 +02:00
parent bd07876a41
commit ea6c3650f3
10 changed files with 936 additions and 875 deletions
@@ -28,7 +28,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
await db.SaveChangesAsync();
var controller = CreateController(db, "user-1");
var result = await controller.SaveApplicationDrafts(job.Id, new JobApplicationsController.SaveApplicationDraftsRequest(" Cover letter body ", " Notes body ", null), CancellationToken.None);
var result = await controller.SaveApplicationDrafts(job.Id, new SaveApplicationDraftsRequest(" Cover letter body ", " Notes body ", null), CancellationToken.None);
Assert.IsType<NoContentResult>(result);
var saved = await db.JobApplications.FirstAsync();
@@ -83,7 +83,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
var result = await controller.GetStatusSuggestion(job.Id, CancellationToken.None);
var ok = Assert.IsType<OkObjectResult>(result.Result);
var dto = Assert.IsType<JobApplicationsController.StatusSuggestionDto>(ok.Value);
var dto = Assert.IsType<StatusSuggestionDto>(ok.Value);
Assert.True(dto.HasSuggestion);
Assert.Equal("Rejected", dto.SuggestedStatus);
}
@@ -114,7 +114,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
var result = await controller.GetStatusSuggestion(job.Id, CancellationToken.None);
var ok = Assert.IsType<OkObjectResult>(result.Result);
var dto = Assert.IsType<JobApplicationsController.StatusSuggestionDto>(ok.Value);
var dto = Assert.IsType<StatusSuggestionDto>(ok.Value);
Assert.False(dto.HasSuggestion);
}
@@ -147,7 +147,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
var result = await controller.GetMatchScore(job.Id, CancellationToken.None);
var ok = Assert.IsType<OkObjectResult>(result.Result);
var dto = Assert.IsType<JobApplicationsController.MatchScoreDto>(ok.Value);
var dto = Assert.IsType<MatchScoreDto>(ok.Value);
Assert.True(dto.HasEnoughSignal);
Assert.True(dto.Score >= 75, $"expected strong score, got {dto.Score}");
Assert.Contains("C#", dto.MatchedKeywords);
@@ -181,7 +181,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
await db.SaveChangesAsync();
var controller = CreateController(db, "user-1");
var request = new JobApplicationsController.CreateJobApplicationRequest(
var request = new CreateJobApplicationRequest(
JobTitle: "Backend Dev",
CompanyId: company.Id,
Status: null,
@@ -237,7 +237,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
await db.SaveChangesAsync();
var controller = CreateController(db, "user-1");
var request = new JobApplicationsController.UpdateJobApplicationRequest(
var request = new UpdateJobApplicationRequest(
JobTitle: "Backend Dev",
CompanyId: company.Id,
Status: "Applied",