feat: add career profile foundation with versioned history
Introduces the Career Workspace's bounded data foundation, additive and backwards-compatible: ApplicationUser.ProfileCvStructureJson stays the authoritative column every existing read path uses; the new CareerProfiles/CareerProfileVersions tables mirror it via ICareerProfileService so future Career Workspace features (variants, history UI) have real tables to build on rather than starting a second migration later. - CareerProfile: one snapshot row per user (Version, ProfileJson). - CareerProfileVersion: append-only history, one row per save (upload/rebuild/improve/reprocess/parse), so a profile edit is never silently lost the way ProfileCvStructureJson overwrites are today. - Stable item IDs assigned to jobs/education/certifications/projects on first save and preserved across later saves -- the prerequisite for CV variants to reference "this job" by identity instead of array position. - CvDateNormalizer: best-effort free-string -> "YYYY-MM" parsing for job/education/certification/project date ranges, kept alongside (never replacing) the original free-string fields. - Both SQLite (dev) and MySQL/MariaDB (prod) reconciler dialects, matching this repo's schema-via-raw-SQL-reconciler convention rather than EF migrations. Job tracking is untouched -- this is entirely within the profile/CV domain per the Career Workspace product boundary.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
namespace JobTrackerApi.Models;
|
||||
|
||||
// The Career Workspace's durable source of truth. Bounded to one row per user for now
|
||||
// (see career-workspace-implementation-roadmap.md Phase F1) -- ProfileJson mirrors
|
||||
// ApplicationUser.ProfileCvStructureJson during the dual-write window and will become
|
||||
// authoritative once every read path is migrated (F5).
|
||||
public sealed class CareerProfile
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string OwnerUserId { get; set; } = string.Empty;
|
||||
public string ProfileJson { get; set; } = string.Empty;
|
||||
public int Version { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
// Append-only history: one row per save, so profile edits are never silently lost.
|
||||
public sealed class CareerProfileVersion
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string OwnerUserId { get; set; } = string.Empty;
|
||||
public int CareerProfileId { get; set; }
|
||||
public CareerProfile? CareerProfile { get; set; }
|
||||
public int Version { get; set; }
|
||||
public string ProfileJson { get; set; } = string.Empty;
|
||||
// Where this version came from: "upload" | "rebuild" | "improve" | "reprocess" | "parse" | "manual".
|
||||
public string Source { get; set; } = string.Empty;
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -49,11 +49,19 @@ public sealed class StructuredCvContact
|
||||
|
||||
public sealed class StructuredCvJob
|
||||
{
|
||||
// Stable item ID (assigned by CareerProfileService on first save). Required for CV variants
|
||||
// to reference "this job" across profile edits, instead of by array position. Nullable/empty
|
||||
// on freshly-parsed or legacy data until the first save assigns it.
|
||||
public string? Id { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Company { get; set; }
|
||||
public string? Location { get; set; }
|
||||
public string? Start { get; set; }
|
||||
public string? End { get; set; }
|
||||
// Best-effort "YYYY-MM" normalization of Start/End, computed alongside Id assignment.
|
||||
// Null when Start/End can't be parsed; the free-string fields above remain the display source.
|
||||
public string? StartDate { get; set; }
|
||||
public string? EndDate { get; set; }
|
||||
public bool IsCurrent { get; set; }
|
||||
public List<string> Bullets { get; set; } = new();
|
||||
public List<string> Skills { get; set; } = new();
|
||||
@@ -61,31 +69,39 @@ public sealed class StructuredCvJob
|
||||
|
||||
public sealed class StructuredCvEducation
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Qualification { get; set; }
|
||||
public string? QualificationLevel { get; set; }
|
||||
public string? Institution { get; set; }
|
||||
public string? Location { get; set; }
|
||||
public string? Start { get; set; }
|
||||
public string? End { get; set; }
|
||||
public string? StartDate { get; set; }
|
||||
public string? EndDate { get; set; }
|
||||
public List<string> Details { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class StructuredCvCertification
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Issuer { get; set; }
|
||||
public string? Location { get; set; }
|
||||
public string? Date { get; set; }
|
||||
public string? DateNormalized { get; set; }
|
||||
public List<string> Details { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class StructuredCvProject
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Role { get; set; }
|
||||
public string? Location { get; set; }
|
||||
public string? Start { get; set; }
|
||||
public string? End { get; set; }
|
||||
public string? StartDate { get; set; }
|
||||
public string? EndDate { get; set; }
|
||||
public List<string> Bullets { get; set; } = new();
|
||||
public List<string> Skills { get; set; } = new();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user