feat(cv): qualify templates and reliable local import processing

This commit is contained in:
cesnimda
2026-10-28 14:36:12 +01:00
parent 73ea888375
commit 3f4db8ca81
69 changed files with 2597 additions and 180 deletions
@@ -45,6 +45,8 @@ public sealed record CoverLetterDto(
int Revision = 1,
CoverLetterDocumentSettings? DocumentSettings = null);
public sealed class CoverLetterThemeAccessException() : Exception("This theme requires Pro.");
public interface IApplicationAssetsService
{
Task<ApplicationCvDto?> GetCvAsync(string ownerUserId, int jobApplicationId, CancellationToken ct);
@@ -293,7 +295,9 @@ public sealed class ApplicationAssetsService : IApplicationAssetsService
public async Task<CoverLetterRenderDto?> PreviewCoverLetterAsync(string ownerUserId, int jobApplicationId, string? text, CoverLetterDocumentSettings? settings, CancellationToken ct)
{
var job = await LoadJobAsync(ownerUserId, jobApplicationId, ct);
return job is null ? null : _letters.Render(text, settings ?? CoverLetterDocumentSettingsJson.Deserialize(job.CoverLetterDocumentSettingsJson));
if (job is null) return null;
await RequireLetterThemeAsync(_db, job, settings, ct);
return _letters.Render(text, settings ?? CoverLetterDocumentSettingsJson.Deserialize(job.CoverLetterDocumentSettingsJson));
}
public async Task<CoverLetterRenderDto?> RenderSavedCoverLetterAsync(string ownerUserId, int jobApplicationId, int? expectedVersion, CancellationToken ct)
@@ -331,6 +335,7 @@ public sealed class ApplicationAssetsService : IApplicationAssetsService
internal static async Task StageCoverLetterAsync(JobTrackerContext db, JobApplication job, string? text,
string source, string? aiAction, CancellationToken ct, CoverLetterDocumentSettings? settings = null)
{
await RequireLetterThemeAsync(db, job, settings, ct);
var currentSettings = CoverLetterDocumentSettingsJson.Serialize(CoverLetterDocumentSettingsJson.Deserialize(job.CoverLetterDocumentSettingsJson));
var nextSettings = settings is null ? currentSettings : CoverLetterDocumentSettingsJson.Serialize(settings);
if (string.Equals(job.CoverLetterText, text, StringComparison.Ordinal) && nextSettings == currentSettings) return;
@@ -362,6 +367,21 @@ public sealed class ApplicationAssetsService : IApplicationAssetsService
if (settings is not null) job.CoverLetterDocumentSettingsJson = nextSettings;
}
private static async Task RequireLetterThemeAsync(JobTrackerContext db, JobApplication job, CoverLetterDocumentSettings? settings, CancellationToken ct)
{
if (settings is null) return;
var next = CoverLetterDocumentSettingsJson.Normalize(settings);
if (next.DesignVersion != 2 || CvThemeCatalog.CanUse(next.ThemeId, false)) return;
var current = CoverLetterDocumentSettingsJson.Deserialize(job.CoverLetterDocumentSettingsJson);
if (current.DesignVersion == 2 && current.ThemeId == next.ThemeId) return;
var roles = await (from userRole in db.UserRoles.AsNoTracking()
join role in db.Roles.AsNoTracking() on userRole.RoleId equals role.Id
where userRole.UserId == job.OwnerUserId && role.Name != null
select role.Name!).ToListAsync(ct);
if (!CvThemeCatalog.CanUse(next.ThemeId, AccountPlans.ForRoles(roles).ProThemes))
throw new CoverLetterThemeAccessException();
}
private Task<JobApplication?> LoadJobAsync(string ownerUserId, int jobApplicationId, CancellationToken ct) =>
_db.JobApplications.AsNoTracking()
.FirstOrDefaultAsync(j => j.Id == jobApplicationId && j.OwnerUserId == ownerUserId, ct);