fix(rd21): guard career and job saves with durable revisions
This commit is contained in:
@@ -459,7 +459,8 @@ Canonical profile:
|
||||
TailoredCvText: job.TailoredCvText,
|
||||
WorkflowSignal: workflowSignal,
|
||||
ShortSummary: job.ShortSummary,
|
||||
FullSummary: fullSummary);
|
||||
FullSummary: fullSummary,
|
||||
Version: job.Version);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -840,6 +841,9 @@ Canonical profile:
|
||||
var job = await _db.JobApplications.Include(j => j.Job).FirstOrDefaultAsync(j => j.Id == id, cancellationToken);
|
||||
if (job is null) return NotFound();
|
||||
|
||||
if (request.ExpectedVersion is null or < 1 || request.ExpectedVersion != job.Version)
|
||||
return Conflict(new { code = "revision_conflict", message = "This job changed or its revision is missing. Your draft was not saved. Reload and review before saving again." });
|
||||
|
||||
var oldStatus = job.Status;
|
||||
var oldResponseReceived = job.ResponseReceived;
|
||||
var oldResponseDate = job.ResponseDate;
|
||||
@@ -850,6 +854,8 @@ Canonical profile:
|
||||
var title = (request.JobTitle ?? "").Trim();
|
||||
if (title.Length == 0) return BadRequest("Job title is required.");
|
||||
if (request.CompanyId <= 0) return BadRequest("Valid companyId is required.");
|
||||
if (!await _db.Companies.AnyAsync(x => x.Id == request.CompanyId, cancellationToken))
|
||||
return BadRequest("Company was not found.");
|
||||
|
||||
job.JobTitle = title;
|
||||
job.CompanyId = request.CompanyId;
|
||||
@@ -926,7 +932,19 @@ Canonical profile:
|
||||
JobLifecycleEvents.RecordStatusChange(_db, job, oldStatus, request.StatusChangedAt ?? DateTime.Now);
|
||||
|
||||
if (job.Job is not null) JobOpportunitySync.Apply(job, job.Job);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
_db.Entry(job).Property(x => x.Version).IsModified = true;
|
||||
try { await _db.SaveChangesAsync(cancellationToken); }
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
_db.ChangeTracker.Clear();
|
||||
return Conflict(new { code = "revision_conflict", message = "This job changed while saving. Your draft was not saved. Reload and review before saving again." });
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
_db.ChangeTracker.Clear();
|
||||
throw;
|
||||
}
|
||||
Response.Headers["X-Job-Version"] = job.Version.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -1397,7 +1415,7 @@ Candidate CV/profile:
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.JobApplicationId == id, cancellationToken);
|
||||
|
||||
return Ok(draft is null ? ToLegacyTailoredCvDraftDto(job) : ToTailoredCvDraftDto(draft));
|
||||
return Ok((draft is null ? ToLegacyTailoredCvDraftDto(job) : ToTailoredCvDraftDto(draft)) with { Revision = job.Version });
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/tailored-cv-preview")]
|
||||
@@ -1486,7 +1504,7 @@ Candidate CV/profile:
|
||||
}
|
||||
|
||||
var draft = await UpsertGeneratedTailoredCvDraftAsync(job, user, mode, cancellationToken);
|
||||
return Ok(ToTailoredCvDraftDto(draft));
|
||||
return Ok(ToTailoredCvDraftDto(draft) with { Revision = job.Version });
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}/tailored-cv-draft")]
|
||||
@@ -1494,6 +1512,8 @@ Candidate CV/profile:
|
||||
{
|
||||
var job = await _db.JobApplications.FirstOrDefaultAsync(j => j.Id == id, cancellationToken);
|
||||
if (job is null) return NotFound();
|
||||
if (request.ExpectedVersion is null or < 1 || request.ExpectedVersion != job.Version)
|
||||
return Conflict(new { code = "revision_conflict", message = "The application changed. Your CV draft was not saved. Reload and review before saving again." });
|
||||
|
||||
var user = await GetCurrentUserAsync(cancellationToken);
|
||||
if (user is null) return Unauthorized();
|
||||
@@ -1531,7 +1551,9 @@ Candidate CV/profile:
|
||||
|
||||
job.TailoredCvText = TailoredCvDraftJson.RenderPlainText(document);
|
||||
job.TailoredCvUpdatedAt = DateTime.UtcNow;
|
||||
_db.Entry(job).Property(x => x.Version).IsModified = true;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
Response.Headers["X-Job-Version"] = job.Version.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -1540,10 +1562,14 @@ Candidate CV/profile:
|
||||
{
|
||||
var job = await _db.JobApplications.FirstOrDefaultAsync(j => j.Id == id, cancellationToken);
|
||||
if (job is null) return NotFound();
|
||||
if (request.ExpectedVersion is null or < 1 || request.ExpectedVersion != job.Version)
|
||||
return Conflict(new { code = "revision_conflict", message = "The application changed. Your draft was not saved. Reload and review before saving again." });
|
||||
|
||||
job.TailoredCvText = string.IsNullOrWhiteSpace(request.TailoredCvText) ? null : request.TailoredCvText.Trim();
|
||||
job.TailoredCvUpdatedAt = job.TailoredCvText is null ? null : DateTime.UtcNow;
|
||||
_db.Entry(job).Property(x => x.Version).IsModified = true;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
Response.Headers["X-Job-Version"] = job.Version.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -1552,6 +1578,8 @@ Candidate CV/profile:
|
||||
{
|
||||
var job = await _db.JobApplications.FirstOrDefaultAsync(j => j.Id == id, cancellationToken);
|
||||
if (job is null) return NotFound();
|
||||
if (request.ExpectedVersion is null or < 1 || request.ExpectedVersion != job.Version)
|
||||
return Conflict(new { code = "revision_conflict", message = "The application changed. Your drafts were not saved. Reload and review before saving again." });
|
||||
|
||||
if (request.CoverLetterText is not null)
|
||||
{
|
||||
@@ -1575,7 +1603,9 @@ Candidate CV/profile:
|
||||
: request.RecruiterMessageDraft.Trim();
|
||||
}
|
||||
|
||||
_db.Entry(job).Property(x => x.Version).IsModified = true;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
Response.Headers["X-Job-Version"] = job.Version.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user