ce76046a29
- consolidate API ownership and remove dead vendor code - add Stripe billing, learning paths, and public CV hardening - add migration, recovery, security, audit, and browser gates
165 lines
5.2 KiB
C#
165 lines
5.2 KiB
C#
using System.ComponentModel.DataAnnotations.Schema;
|
|
using System.Text.Json;
|
|
|
|
namespace JobTrackerApi.Models;
|
|
|
|
// Phase 3: relational children of CareerProfile — the editable master career profile.
|
|
// See docs/architecture/career-profile-model.md.
|
|
//
|
|
// Design notes shared by all child entities:
|
|
// - OwnerUserId is denormalized so the tenant global query filter applies directly (same pattern
|
|
// as every other owned entity).
|
|
// - ItemKey carries the stable item id from the StructuredCvProfile blob, so a row keeps its
|
|
// identity across imports/edits and future CV variants can reference "this experience".
|
|
// - SortOrder makes ordering explicit (the blob relied on array position).
|
|
// - List fields persist as JSON string columns (plain TEXT — reconciler-friendly on both SQLite
|
|
// and MySQL) exposed via [NotMapped] accessors. Children are replaced wholesale on save, so
|
|
// fine-grained change tracking of the lists is not needed.
|
|
|
|
/// <summary>Base fields every CareerProfile child shares.</summary>
|
|
public abstract class CareerChildEntity
|
|
{
|
|
public int Id { get; set; }
|
|
public int CareerProfileId { get; set; }
|
|
public CareerProfile? CareerProfile { get; set; }
|
|
public string OwnerUserId { get; set; } = string.Empty;
|
|
|
|
/// <summary>Stable identity carried from StructuredCvProfile so references survive edits.</summary>
|
|
public string ItemKey { get; set; } = string.Empty;
|
|
|
|
public int SortOrder { get; set; }
|
|
}
|
|
|
|
internal static class CareerJson
|
|
{
|
|
public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
|
|
|
|
public static List<string> ReadList(string? json)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json)) return new();
|
|
try { return JsonSerializer.Deserialize<List<string>>(json, Options) ?? new(); }
|
|
catch { return new(); }
|
|
}
|
|
|
|
public static string WriteList(List<string> items) => JsonSerializer.Serialize(items ?? new(), Options);
|
|
}
|
|
|
|
public sealed class CareerExperience : CareerChildEntity
|
|
{
|
|
public string? Title { get; set; }
|
|
public string? Company { get; set; }
|
|
public string? Location { get; set; }
|
|
// Free-text period kept alongside best-effort "YYYY-MM" normalization; neither replaces the other.
|
|
public string? Start { get; set; }
|
|
public string? End { get; set; }
|
|
public string? StartDate { get; set; }
|
|
public string? EndDate { get; set; }
|
|
public bool IsCurrent { get; set; }
|
|
|
|
public string BulletsJson { get; set; } = "[]";
|
|
public string SkillsJson { get; set; } = "[]";
|
|
|
|
[NotMapped]
|
|
public List<string> Bullets
|
|
{
|
|
get => CareerJson.ReadList(BulletsJson);
|
|
set => BulletsJson = CareerJson.WriteList(value);
|
|
}
|
|
|
|
[NotMapped]
|
|
public List<string> Skills
|
|
{
|
|
get => CareerJson.ReadList(SkillsJson);
|
|
set => SkillsJson = CareerJson.WriteList(value);
|
|
}
|
|
}
|
|
|
|
public sealed class CareerEducation : CareerChildEntity
|
|
{
|
|
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 string DetailsJson { get; set; } = "[]";
|
|
|
|
[NotMapped]
|
|
public List<string> Details
|
|
{
|
|
get => CareerJson.ReadList(DetailsJson);
|
|
set => DetailsJson = CareerJson.WriteList(value);
|
|
}
|
|
}
|
|
|
|
public sealed class CareerSkill : CareerChildEntity
|
|
{
|
|
public string? Name { get; set; }
|
|
public string? Category { get; set; }
|
|
public string? Proficiency { get; set; }
|
|
}
|
|
|
|
public sealed class CareerProject : CareerChildEntity
|
|
{
|
|
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 string BulletsJson { get; set; } = "[]";
|
|
public string SkillsJson { get; set; } = "[]";
|
|
public string LinksJson { get; set; } = "[]";
|
|
|
|
[NotMapped]
|
|
public List<string> Bullets
|
|
{
|
|
get => CareerJson.ReadList(BulletsJson);
|
|
set => BulletsJson = CareerJson.WriteList(value);
|
|
}
|
|
|
|
[NotMapped]
|
|
public List<string> Skills
|
|
{
|
|
get => CareerJson.ReadList(SkillsJson);
|
|
set => SkillsJson = CareerJson.WriteList(value);
|
|
}
|
|
|
|
[NotMapped]
|
|
public List<string> Links
|
|
{
|
|
get => CareerJson.ReadList(LinksJson);
|
|
set => LinksJson = CareerJson.WriteList(value);
|
|
}
|
|
}
|
|
|
|
public sealed class CareerCertification : CareerChildEntity
|
|
{
|
|
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 string DetailsJson { get; set; } = "[]";
|
|
|
|
[NotMapped]
|
|
public List<string> Details
|
|
{
|
|
get => CareerJson.ReadList(DetailsJson);
|
|
set => DetailsJson = CareerJson.WriteList(value);
|
|
}
|
|
}
|
|
|
|
public sealed class CareerLanguage : CareerChildEntity
|
|
{
|
|
public string? Name { get; set; }
|
|
public string? Level { get; set; }
|
|
public string? Notes { get; set; }
|
|
}
|