fix(career): preserve reviewed profile values
CI and Deploy / test (pull_request) Successful in 4m39s
CI and Deploy / deploy (pull_request) Has been skipped

Keep extraction heuristics out of manual save, version, and import paths so reviewed locations, URLs, dates, and languages round-trip unchanged.
This commit is contained in:
cesnimda
2026-08-15 16:56:31 +02:00
parent a6cffe0473
commit f0b9b222ff
15 changed files with 277 additions and 37 deletions
@@ -51,7 +51,7 @@ public sealed class CareerProfileController : ControllerBase
var user = await _users.GetUserAsync(User);
if (user is null) return StatusCode(501, "The career profile can only be edited on local accounts.");
var profile = StructuredCvProfileJson.Normalize(request?.Profile);
var profile = StructuredCvProfileJson.NormalizeForPersistence(request?.Profile);
var error = CareerProfileValidator.Validate(profile);
if (error is not null) return BadRequest(error);
@@ -59,7 +59,7 @@ public sealed class CareerProfileController : ControllerBase
// Keep the derived projection in sync for legacy readers. CvText (the raw imported text) is
// part of the career profile and is set here too; identity fields are never touched.
user.ProfileCvStructureJson = StructuredCvProfileJson.Serialize(saved);
user.ProfileCvStructureJson = StructuredCvProfileJson.SerializePersisted(saved);
if (request?.CvText is not null) user.ProfileCvText = string.IsNullOrWhiteSpace(request.CvText) ? null : request.CvText;
var res = await _users.UpdateAsync(user);
if (!res.Succeeded) return BadRequest(string.Join("; ", res.Errors.Select(e => e.Description)));
@@ -100,7 +100,7 @@ public sealed class CareerProfileController : ControllerBase
var restored = await _career.RestoreVersionAsync(user.Id, version, cancellationToken);
if (restored is null) return NotFound($"Version {version} was not found.");
user.ProfileCvStructureJson = StructuredCvProfileJson.Serialize(restored);
user.ProfileCvStructureJson = StructuredCvProfileJson.SerializePersisted(restored);
var res = await _users.UpdateAsync(user);
if (!res.Succeeded) return BadRequest(string.Join("; ", res.Errors.Select(e => e.Description)));
@@ -103,7 +103,7 @@ namespace JobTrackerApi.Controllers
private async Task<TailoredCvDraft> UpsertGeneratedTailoredCvDraftAsync(JobApplication job, ApplicationUser user, string? mode, CancellationToken cancellationToken)
{
var structured = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
var structured = StructuredCvProfileJson.DeserializePersisted(user.ProfileCvStructureJson);
var jobText = string.Join("\n\n", new[] { job.JobTitle, job.Company?.Name, job.Description, job.TranslatedDescription, job.Notes, job.ShortSummary, job.JobUrl }
.Where(value => !string.IsNullOrWhiteSpace(value)));
var structuredCvContext = BuildStructuredCvContext(user);
@@ -1312,7 +1312,7 @@ Canonical profile:
// Builds CV text grouped by section so match coverage can show *where* the evidence sits.
private static Dictionary<string, string> BuildCvSections(ApplicationUser? user)
{
var structured = StructuredCvProfileJson.Deserialize(user?.ProfileCvStructureJson);
var structured = StructuredCvProfileJson.DeserializePersisted(user?.ProfileCvStructureJson);
var sections = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
void Add(string name, IEnumerable<string?> values)
@@ -1752,7 +1752,7 @@ Candidate CV/profile:
return BadRequest("Add your profile CV text on the Profile page before generating a tailored CV draft.");
}
var structured = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
var structured = StructuredCvProfileJson.DeserializePersisted(user.ProfileCvStructureJson);
if (structured.Summary.Count == 0 && structured.Jobs.Count == 0 && structured.Skills.Count == 0)
{
return BadRequest("Build and review your canonical structured CV on the Profile page before generating a tailored draft.");
@@ -253,7 +253,7 @@ public sealed partial class ProfileCvController : ControllerBase
merged.Metadata.UpdatedAtUtc = DateTimeOffset.UtcNow;
await _careerProfileService.SaveVersionAsync(user.Id, merged, $"{run.Trigger}:accepted", HttpContext.RequestAborted);
user.ProfileCvStructureJson = StructuredCvProfileJson.Serialize(merged);
user.ProfileCvStructureJson = StructuredCvProfileJson.SerializePersisted(merged);
if (string.IsNullOrWhiteSpace(user.ProfileCvText)) user.ProfileCvText = run.NormalizedText;
user.CurrentCvExtractionRunId = run.Id;
user.CurrentCvProfileVersion = merged.Metadata.ProfileVersion;
@@ -323,7 +323,7 @@ public sealed partial class ProfileCvController : ControllerBase
var user = await _users.GetUserAsync(User);
if (user is null) return Unauthorized();
var structuredCv = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
var structuredCv = StructuredCvProfileJson.DeserializePersisted(user.ProfileCvStructureJson);
var sourceText = string.IsNullOrWhiteSpace(request.SourceText)
? (string.IsNullOrWhiteSpace(user.ProfileCvText) ? null : user.ProfileCvText.Trim())
: request.SourceText.Trim();
@@ -427,7 +427,7 @@ public sealed partial class ProfileCvController : ControllerBase
var user = await _users.GetUserAsync(User);
if (user is null) return Unauthorized();
var structuredCv = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
var structuredCv = StructuredCvProfileJson.DeserializePersisted(user.ProfileCvStructureJson);
var sourceText = string.IsNullOrWhiteSpace(request.SourceText)
? (string.IsNullOrWhiteSpace(user.ProfileCvText) ? null : user.ProfileCvText.Trim())
: request.SourceText.Trim();