feat(career): CV builder backend — data-driven theme engine + variant model
CI and Deploy / test (push) Failing after 1m51s
CI and Deploy / deploy (push) Has been skipped

Phase 4 foundation. A CvVariant is a lens over the master CareerProfile
(section order/visibility, per-item overrides keyed by ItemKey, theme +
builder settings) — it references career data, never duplicates it. One
renderer (ThemedCvRenderer) draws every theme; a theme is pure data
(CvThemeCatalog, 8 professional themes), so adding a theme needs no renderer
change. Autosave version history + non-destructive restore, public CV via
/api/public-cv/{slug} (anonymous, noindex, filter-bypassing owner load), and
an AI-assist endpoint reusing the existing provider abstraction (suggestions
only, never auto-applied).

- Models: CvVariant/CvVariantVersion, CvVariantSettings, CvTheme + catalog
- Services: CvVariantResolver (profile+lens -> render model), ThemedCvRenderer,
  CvVariantService, CareerProfileService.LoadStructuredForOwnerAsync (public)
- API: CvVariantController (/api/cv), PublicCvController (/api/public-cv)
- Migration AddCvVariants (2 self-contained tables; verified applied on the
  running container), 16 tests (resolver/renderer/service), 296 backend green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-18 09:52:58 +02:00
parent 707d8c59d2
commit a3e18e4b44
15 changed files with 3842 additions and 0 deletions
+29
View File
@@ -42,6 +42,8 @@ namespace JobTrackerApi.Data
public DbSet<CareerLanguage> CareerLanguages => Set<CareerLanguage>();
public DbSet<InterviewPrepNote> InterviewPrepNotes => Set<InterviewPrepNote>();
public DbSet<AiWorkspaceNote> AiWorkspaceNotes => Set<AiWorkspaceNote>();
public DbSet<CvVariant> CvVariants => Set<CvVariant>();
public DbSet<CvVariantVersion> CvVariantVersions => Set<CvVariantVersion>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -288,6 +290,33 @@ namespace JobTrackerApi.Data
.WithMany()
.HasForeignKey(x => x.JobApplicationId)
.OnDelete(DeleteBehavior.Cascade);
// Phase 4: CV variants (a lens over the master profile) + their autosave history.
// docs/architecture/cv-builder.md. Same deny-on-null tenant filter as everything else. A
// variant references a job application (reference-not-ownership, SetNull on delete) and never
// owns career data. PublicSlug is globally unique so /cv/{slug} can resolve it anonymously.
modelBuilder.Entity<CvVariant>()
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
modelBuilder.Entity<CvVariant>()
.HasIndex(x => x.PublicSlug)
.IsUnique();
modelBuilder.Entity<CvVariant>()
.HasIndex(x => new { x.OwnerUserId, x.UpdatedAtUtc });
modelBuilder.Entity<CvVariant>()
.HasOne(x => x.JobApplication)
.WithMany()
.HasForeignKey(x => x.JobApplicationId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<CvVariantVersion>()
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
modelBuilder.Entity<CvVariantVersion>()
.HasIndex(x => new { x.CvVariantId, x.Version });
modelBuilder.Entity<CvVariantVersion>()
.HasOne(x => x.CvVariant)
.WithMany()
.HasForeignKey(x => x.CvVariantId)
.OnDelete(DeleteBehavior.Cascade);
}
// Common config for CareerProfile's relational children. The 1:many FK + cascade delete is