refactor: Rename TrueCV to RealCV throughout codebase
- Renamed all directories (TrueCV.* -> RealCV.*) - Renamed all project files (.csproj) - Renamed solution file (TrueCV.sln -> RealCV.sln) - Updated all namespaces in C# and Razor files - Updated project references - Updated CSS variable names 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
namespace RealCV.Infrastructure.Configuration;
|
||||
|
||||
public sealed class AnthropicSettings
|
||||
{
|
||||
public const string SectionName = "Anthropic";
|
||||
|
||||
public required string ApiKey { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace RealCV.Infrastructure.Configuration;
|
||||
|
||||
public sealed class AzureBlobSettings
|
||||
{
|
||||
public const string SectionName = "AzureBlob";
|
||||
|
||||
public required string ConnectionString { get; init; }
|
||||
public required string ContainerName { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace RealCV.Infrastructure.Configuration;
|
||||
|
||||
public sealed class CompaniesHouseSettings
|
||||
{
|
||||
public const string SectionName = "CompaniesHouse";
|
||||
|
||||
public required string BaseUrl { get; init; }
|
||||
public required string ApiKey { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RealCV.Infrastructure.Configuration;
|
||||
|
||||
public sealed class LocalStorageSettings
|
||||
{
|
||||
public const string SectionName = "LocalStorage";
|
||||
|
||||
public string StoragePath { get; set; } = "./uploads";
|
||||
}
|
||||
127
src/RealCV.Infrastructure/Data/ApplicationDbContext.cs
Normal file
127
src/RealCV.Infrastructure/Data/ApplicationDbContext.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Infrastructure.Identity;
|
||||
|
||||
namespace RealCV.Infrastructure.Data;
|
||||
|
||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
|
||||
{
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<CVCheck> CVChecks => Set<CVCheck>();
|
||||
public DbSet<CVFlag> CVFlags => Set<CVFlag>();
|
||||
public DbSet<CompanyCache> CompanyCache => Set<CompanyCache>();
|
||||
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
ConfigureApplicationUser(builder);
|
||||
ConfigureCVCheck(builder);
|
||||
ConfigureCVFlag(builder);
|
||||
ConfigureCompanyCache(builder);
|
||||
ConfigureAuditLog(builder);
|
||||
}
|
||||
|
||||
private static void ConfigureApplicationUser(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<ApplicationUser>(entity =>
|
||||
{
|
||||
entity.Property(u => u.Plan)
|
||||
.HasConversion<string>()
|
||||
.HasMaxLength(32);
|
||||
|
||||
entity.Property(u => u.StripeCustomerId)
|
||||
.HasMaxLength(256);
|
||||
|
||||
entity.HasMany(u => u.CVChecks)
|
||||
.WithOne()
|
||||
.HasForeignKey(c => c.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureCVCheck(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<CVCheck>(entity =>
|
||||
{
|
||||
entity.Property(c => c.Status)
|
||||
.HasConversion<string>()
|
||||
.HasMaxLength(32);
|
||||
|
||||
entity.HasIndex(c => c.UserId)
|
||||
.HasDatabaseName("IX_CVChecks_UserId");
|
||||
|
||||
entity.HasIndex(c => c.Status)
|
||||
.HasDatabaseName("IX_CVChecks_Status");
|
||||
|
||||
entity.HasMany(c => c.Flags)
|
||||
.WithOne(f => f.CVCheck)
|
||||
.HasForeignKey(f => f.CVCheckId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureCVFlag(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<CVFlag>(entity =>
|
||||
{
|
||||
entity.Property(f => f.Category)
|
||||
.HasConversion<string>()
|
||||
.HasMaxLength(32);
|
||||
|
||||
entity.Property(f => f.Severity)
|
||||
.HasConversion<string>()
|
||||
.HasMaxLength(32);
|
||||
|
||||
entity.HasIndex(f => f.CVCheckId)
|
||||
.HasDatabaseName("IX_CVFlags_CVCheckId");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureCompanyCache(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<CompanyCache>(entity =>
|
||||
{
|
||||
entity.HasKey(c => c.CompanyNumber);
|
||||
|
||||
entity.Property(c => c.CompanyNumber)
|
||||
.HasMaxLength(32);
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureAuditLog(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<AuditLog>(entity =>
|
||||
{
|
||||
entity.HasIndex(a => a.UserId)
|
||||
.HasDatabaseName("IX_AuditLogs_UserId");
|
||||
|
||||
entity.HasIndex(a => a.CreatedAt)
|
||||
.HasDatabaseName("IX_AuditLogs_CreatedAt");
|
||||
|
||||
entity.HasIndex(a => a.Action)
|
||||
.HasDatabaseName("IX_AuditLogs_Action");
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var newCVChecks = ChangeTracker.Entries<CVCheck>()
|
||||
.Where(e => e.State == EntityState.Added)
|
||||
.Select(e => e.Entity);
|
||||
|
||||
foreach (var cvCheck in newCVChecks)
|
||||
{
|
||||
cvCheck.CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
return await base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
480
src/RealCV.Infrastructure/Data/Migrations/20260118182916_InitialCreate.Designer.cs
generated
Normal file
480
src/RealCV.Infrastructure/Data/Migrations/20260118182916_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,480 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RealCV.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260118182916_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.23")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("BlobUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExtractedDataJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("OriginalFileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("ReportJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("UserId1")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int?>("VeracityScore")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_CVChecks_Status");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("IX_CVChecks_UserId");
|
||||
|
||||
b.HasIndex("UserId1");
|
||||
|
||||
b.ToTable("CVChecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("CVCheckId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<int>("ScoreImpact")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CVCheckId")
|
||||
.HasDatabaseName("IX_CVFlags_CVCheckId");
|
||||
|
||||
b.ToTable("CVFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CompanyCache", b =>
|
||||
{
|
||||
b.Property<string>("CompanyNumber")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<DateTime>("CachedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CompanyName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<DateOnly?>("DissolutionDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly?>("IncorporationDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("CompanyNumber");
|
||||
|
||||
b.ToTable("CompanyCache");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("ChecksUsedThisMonth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<int>("Plan")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("StripeCustomerId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ChecksUsedThisMonth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Plan")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("StripeCustomerId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany("CVChecks")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RealCV.Domain.Entities.User", null)
|
||||
.WithMany("CVChecks")
|
||||
.HasForeignKey("UserId1");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("CVCheckId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CVCheck");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Navigation("Flags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("CVChecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Navigation("CVChecks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Plan = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
StripeCustomerId = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
ChecksUsedThisMonth = table.Column<int>(type: "int", nullable: false),
|
||||
UserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
SecurityStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
LockoutEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CompanyCache",
|
||||
columns: table => new
|
||||
{
|
||||
CompanyNumber = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
CompanyName = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
|
||||
Status = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
IncorporationDate = table.Column<DateOnly>(type: "date", nullable: true),
|
||||
DissolutionDate = table.Column<DateOnly>(type: "date", nullable: true),
|
||||
CachedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CompanyCache", x => x.CompanyNumber);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "User",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Plan = table.Column<int>(type: "int", nullable: false),
|
||||
StripeCustomerId = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
ChecksUsedThisMonth = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_User", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
RoleId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
RoleId = table.Column<Guid>(type: "uniqueidentifier", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserTokens",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
Value = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CVChecks",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
OriginalFileName = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
|
||||
BlobUrl = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: false),
|
||||
Status = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
ExtractedDataJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
VeracityScore = table.Column<int>(type: "int", nullable: true),
|
||||
ReportJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CompletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
UserId1 = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CVChecks", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CVChecks_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_CVChecks_User_UserId1",
|
||||
column: x => x.UserId1,
|
||||
principalTable: "User",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CVFlags",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
CVCheckId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Category = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
Severity = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: false),
|
||||
ScoreImpact = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CVFlags", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CVFlags_CVChecks_CVCheckId",
|
||||
column: x => x.CVCheckId,
|
||||
principalTable: "CVChecks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetRoleClaims_RoleId",
|
||||
table: "AspNetRoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
table: "AspNetRoles",
|
||||
column: "NormalizedName",
|
||||
unique: true,
|
||||
filter: "[NormalizedName] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserClaims_UserId",
|
||||
table: "AspNetUserClaims",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserLogins_UserId",
|
||||
table: "AspNetUserLogins",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserRoles_RoleId",
|
||||
table: "AspNetUserRoles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedUserName",
|
||||
unique: true,
|
||||
filter: "[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CVChecks_Status",
|
||||
table: "CVChecks",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CVChecks_UserId",
|
||||
table: "CVChecks",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CVChecks_UserId1",
|
||||
table: "CVChecks",
|
||||
column: "UserId1");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CVFlags_CVCheckId",
|
||||
table: "CVFlags",
|
||||
column: "CVCheckId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoleClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserLogins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CompanyCache");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CVFlags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CVChecks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "User");
|
||||
}
|
||||
}
|
||||
}
|
||||
456
src/RealCV.Infrastructure/Data/Migrations/20260120191035_AddProcessingStageToCV.Designer.cs
generated
Normal file
456
src/RealCV.Infrastructure/Data/Migrations/20260120191035_AddProcessingStageToCV.Designer.cs
generated
Normal file
@@ -0,0 +1,456 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RealCV.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260120191035_AddProcessingStageToCV")]
|
||||
partial class AddProcessingStageToCV
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.23")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("BlobUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExtractedDataJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("OriginalFileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("ProcessingStage")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ReportJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int?>("VeracityScore")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_CVChecks_Status");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("IX_CVChecks_UserId");
|
||||
|
||||
b.ToTable("CVChecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("CVCheckId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<int>("ScoreImpact")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CVCheckId")
|
||||
.HasDatabaseName("IX_CVFlags_CVCheckId");
|
||||
|
||||
b.ToTable("CVFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CompanyCache", b =>
|
||||
{
|
||||
b.Property<string>("CompanyNumber")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("AccountsCategory")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CachedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CompanyName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("CompanyType")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateOnly?>("DissolutionDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly?>("IncorporationDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("SicCodesJson")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("CompanyNumber");
|
||||
|
||||
b.ToTable("CompanyCache");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ChecksUsedThisMonth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Plan")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("StripeCustomerId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany("CVChecks")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("CVCheckId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CVCheck");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Navigation("Flags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Navigation("CVChecks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddProcessingStageToCV : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_CVChecks_User_UserId1",
|
||||
table: "CVChecks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "User");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_CVChecks_UserId1",
|
||||
table: "CVChecks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserId1",
|
||||
table: "CVChecks");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ProcessingStage",
|
||||
table: "CVChecks",
|
||||
type: "nvarchar(64)",
|
||||
maxLength: 64,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AccountsCategory",
|
||||
table: "CompanyCache",
|
||||
type: "nvarchar(64)",
|
||||
maxLength: 64,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CompanyType",
|
||||
table: "CompanyCache",
|
||||
type: "nvarchar(64)",
|
||||
maxLength: 64,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SicCodesJson",
|
||||
table: "CompanyCache",
|
||||
type: "nvarchar(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProcessingStage",
|
||||
table: "CVChecks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AccountsCategory",
|
||||
table: "CompanyCache");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CompanyType",
|
||||
table: "CompanyCache");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SicCodesJson",
|
||||
table: "CompanyCache");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "UserId1",
|
||||
table: "CVChecks",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "User",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ChecksUsedThisMonth = table.Column<int>(type: "int", nullable: false),
|
||||
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Plan = table.Column<int>(type: "int", nullable: false),
|
||||
StripeCustomerId = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_User", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CVChecks_UserId1",
|
||||
table: "CVChecks",
|
||||
column: "UserId1");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_CVChecks_User_UserId1",
|
||||
table: "CVChecks",
|
||||
column: "UserId1",
|
||||
principalTable: "User",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
502
src/RealCV.Infrastructure/Data/Migrations/20260120194532_AddAuditLogTable.Designer.cs
generated
Normal file
502
src/RealCV.Infrastructure/Data/Migrations/20260120194532_AddAuditLogTable.Designer.cs
generated
Normal file
@@ -0,0 +1,502 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RealCV.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260120194532_AddAuditLogTable")]
|
||||
partial class AddAuditLogTable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.23")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.AuditLog", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Details")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<Guid?>("EntityId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("EntityType")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Action")
|
||||
.HasDatabaseName("IX_AuditLogs_Action");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("IX_AuditLogs_CreatedAt");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("IX_AuditLogs_UserId");
|
||||
|
||||
b.ToTable("AuditLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("BlobUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExtractedDataJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("OriginalFileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("ProcessingStage")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ReportJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int?>("VeracityScore")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_CVChecks_Status");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("IX_CVChecks_UserId");
|
||||
|
||||
b.ToTable("CVChecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("CVCheckId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<int>("ScoreImpact")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CVCheckId")
|
||||
.HasDatabaseName("IX_CVFlags_CVCheckId");
|
||||
|
||||
b.ToTable("CVFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CompanyCache", b =>
|
||||
{
|
||||
b.Property<string>("CompanyNumber")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("AccountsCategory")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CachedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CompanyName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("CompanyType")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateOnly?>("DissolutionDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly?>("IncorporationDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("SicCodesJson")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("CompanyNumber");
|
||||
|
||||
b.ToTable("CompanyCache");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ChecksUsedThisMonth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Plan")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("StripeCustomerId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany("CVChecks")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("CVCheckId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CVCheck");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Navigation("Flags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Navigation("CVChecks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAuditLogTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AuditLogs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Action = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
EntityType = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
EntityId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Details = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
|
||||
IpAddress = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AuditLogs", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuditLogs_Action",
|
||||
table: "AuditLogs",
|
||||
column: "Action");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuditLogs_CreatedAt",
|
||||
table: "AuditLogs",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuditLogs_UserId",
|
||||
table: "AuditLogs",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AuditLogs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RealCV.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.23")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.AuditLog", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Details")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<Guid?>("EntityId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("EntityType")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Action")
|
||||
.HasDatabaseName("IX_AuditLogs_Action");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("IX_AuditLogs_CreatedAt");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("IX_AuditLogs_UserId");
|
||||
|
||||
b.ToTable("AuditLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("BlobUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExtractedDataJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("OriginalFileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("ProcessingStage")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ReportJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int?>("VeracityScore")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_CVChecks_Status");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("IX_CVChecks_UserId");
|
||||
|
||||
b.ToTable("CVChecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("CVCheckId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<int>("ScoreImpact")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CVCheckId")
|
||||
.HasDatabaseName("IX_CVFlags_CVCheckId");
|
||||
|
||||
b.ToTable("CVFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CompanyCache", b =>
|
||||
{
|
||||
b.Property<string>("CompanyNumber")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("AccountsCategory")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CachedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CompanyName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("CompanyType")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateOnly?>("DissolutionDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly?>("IncorporationDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("SicCodesJson")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("CompanyNumber");
|
||||
|
||||
b.ToTable("CompanyCache");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ChecksUsedThisMonth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Plan")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("StripeCustomerId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany("CVChecks")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.HasOne("RealCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("CVCheckId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CVCheck");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Navigation("Flags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Navigation("CVChecks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
132
src/RealCV.Infrastructure/DependencyInjection.cs
Normal file
132
src/RealCV.Infrastructure/DependencyInjection.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using Hangfire;
|
||||
using Hangfire.SqlServer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
using RealCV.Infrastructure.Data;
|
||||
using RealCV.Infrastructure.ExternalApis;
|
||||
using RealCV.Infrastructure.Jobs;
|
||||
using RealCV.Infrastructure.Services;
|
||||
|
||||
namespace RealCV.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// Configure DbContext with SQL Server
|
||||
// AddDbContextFactory enables thread-safe parallel operations
|
||||
services.AddDbContextFactory<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer(
|
||||
configuration.GetConnectionString("DefaultConnection"),
|
||||
sqlOptions =>
|
||||
{
|
||||
sqlOptions.EnableRetryOnFailure(
|
||||
maxRetryCount: 3,
|
||||
maxRetryDelay: TimeSpan.FromSeconds(30),
|
||||
errorNumbersToAdd: null);
|
||||
}));
|
||||
|
||||
// Also register DbContext for scoped injection (non-parallel scenarios)
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer(
|
||||
configuration.GetConnectionString("DefaultConnection"),
|
||||
sqlOptions =>
|
||||
{
|
||||
sqlOptions.EnableRetryOnFailure(
|
||||
maxRetryCount: 3,
|
||||
maxRetryDelay: TimeSpan.FromSeconds(30),
|
||||
errorNumbersToAdd: null);
|
||||
}));
|
||||
|
||||
// Configure Hangfire with SQL Server storage
|
||||
services.AddHangfire(config => config
|
||||
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
|
||||
.UseSimpleAssemblyNameTypeSerializer()
|
||||
.UseRecommendedSerializerSettings()
|
||||
.UseSqlServerStorage(
|
||||
configuration.GetConnectionString("HangfireConnection"),
|
||||
new SqlServerStorageOptions
|
||||
{
|
||||
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
|
||||
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
|
||||
QueuePollInterval = TimeSpan.Zero,
|
||||
UseRecommendedIsolationLevel = true,
|
||||
DisableGlobalLocks = true
|
||||
}));
|
||||
|
||||
services.AddHangfireServer();
|
||||
|
||||
// Configure options
|
||||
services.Configure<CompaniesHouseSettings>(
|
||||
configuration.GetSection(CompaniesHouseSettings.SectionName));
|
||||
|
||||
services.Configure<AnthropicSettings>(
|
||||
configuration.GetSection(AnthropicSettings.SectionName));
|
||||
|
||||
services.Configure<AzureBlobSettings>(
|
||||
configuration.GetSection(AzureBlobSettings.SectionName));
|
||||
|
||||
services.Configure<LocalStorageSettings>(
|
||||
configuration.GetSection(LocalStorageSettings.SectionName));
|
||||
|
||||
// Configure HttpClient for CompaniesHouseClient with retry policy
|
||||
services.AddHttpClient<CompaniesHouseClient>((serviceProvider, client) =>
|
||||
{
|
||||
var settings = configuration
|
||||
.GetSection(CompaniesHouseSettings.SectionName)
|
||||
.Get<CompaniesHouseSettings>();
|
||||
|
||||
if (settings is not null)
|
||||
{
|
||||
client.BaseAddress = new Uri(settings.BaseUrl);
|
||||
}
|
||||
})
|
||||
.AddPolicyHandler(GetRetryPolicy());
|
||||
|
||||
// Register services
|
||||
services.AddScoped<ICVParserService, CVParserService>();
|
||||
services.AddScoped<ICompanyNameMatcherService, AICompanyNameMatcherService>();
|
||||
services.AddScoped<ICompanyVerifierService, CompanyVerifierService>();
|
||||
services.AddScoped<IEducationVerifierService, EducationVerifierService>();
|
||||
services.AddScoped<ITimelineAnalyserService, TimelineAnalyserService>();
|
||||
services.AddScoped<ICVCheckService, CVCheckService>();
|
||||
services.AddScoped<IUserContextService, UserContextService>();
|
||||
services.AddScoped<IAuditService, AuditService>();
|
||||
|
||||
// Register file storage - use local storage if configured, otherwise Azure
|
||||
var useLocalStorage = configuration.GetValue<bool>("UseLocalStorage");
|
||||
if (useLocalStorage)
|
||||
{
|
||||
services.AddScoped<IFileStorageService, LocalFileStorageService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddScoped<IFileStorageService, FileStorageService>();
|
||||
}
|
||||
|
||||
// Register Hangfire jobs
|
||||
services.AddTransient<ProcessCVCheckJob>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
||||
{
|
||||
return HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
|
||||
.WaitAndRetryAsync(
|
||||
retryCount: 3,
|
||||
sleepDurationProvider: retryAttempt =>
|
||||
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
||||
onRetry: (outcome, timespan, retryAttempt, context) =>
|
||||
{
|
||||
// Logging could be added here via ILogger if injected
|
||||
});
|
||||
}
|
||||
}
|
||||
248
src/RealCV.Infrastructure/ExternalApis/CompaniesHouseClient.cs
Normal file
248
src/RealCV.Infrastructure/ExternalApis/CompaniesHouseClient.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RealCV.Application.DTOs;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
|
||||
namespace RealCV.Infrastructure.ExternalApis;
|
||||
|
||||
public sealed class CompaniesHouseClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<CompaniesHouseClient> _logger;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public CompaniesHouseClient(
|
||||
HttpClient httpClient,
|
||||
IOptions<CompaniesHouseSettings> settings,
|
||||
ILogger<CompaniesHouseClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
||||
var apiKey = settings.Value.ApiKey;
|
||||
var authValue = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{apiKey}:"));
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authValue);
|
||||
_httpClient.BaseAddress = new Uri(settings.Value.BaseUrl);
|
||||
}
|
||||
|
||||
public async Task<CompaniesHouseSearchResponse?> SearchCompaniesAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(query);
|
||||
|
||||
var encodedQuery = Uri.EscapeDataString(query);
|
||||
var requestUrl = $"/search/companies?q={encodedQuery}";
|
||||
|
||||
_logger.LogDebug("Searching Companies House for: {Query}", query);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(requestUrl, cancellationToken);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.LogWarning("Rate limit exceeded for Companies House API");
|
||||
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.");
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<CompaniesHouseSearchResponse>(
|
||||
JsonOptions,
|
||||
cancellationToken);
|
||||
|
||||
_logger.LogDebug("Found {Count} companies matching query: {Query}",
|
||||
result?.Items?.Count ?? 0, query);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.LogWarning("Rate limit exceeded for Companies House API");
|
||||
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CompaniesHouseCompany?> GetCompanyAsync(
|
||||
string companyNumber,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(companyNumber);
|
||||
|
||||
var requestUrl = $"/company/{companyNumber}";
|
||||
|
||||
_logger.LogDebug("Fetching company details for: {CompanyNumber}", companyNumber);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(requestUrl, cancellationToken);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
_logger.LogDebug("Company not found: {CompanyNumber}", companyNumber);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.LogWarning("Rate limit exceeded for Companies House API");
|
||||
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.");
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<CompaniesHouseCompany>(
|
||||
JsonOptions,
|
||||
cancellationToken);
|
||||
|
||||
_logger.LogDebug("Retrieved company: {CompanyName} ({CompanyNumber})",
|
||||
result?.CompanyName, companyNumber);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.LogWarning("Rate limit exceeded for Companies House API");
|
||||
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CompaniesHouseOfficersResponse?> GetOfficersAsync(
|
||||
string companyNumber,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(companyNumber);
|
||||
|
||||
var requestUrl = $"/company/{companyNumber}/officers";
|
||||
|
||||
_logger.LogDebug("Fetching officers for company: {CompanyNumber}", companyNumber);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(requestUrl, cancellationToken);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
_logger.LogDebug("No officers found for company: {CompanyNumber}", companyNumber);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.LogWarning("Rate limit exceeded for Companies House API");
|
||||
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.");
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<CompaniesHouseOfficersResponse>(
|
||||
JsonOptions,
|
||||
cancellationToken);
|
||||
|
||||
_logger.LogDebug("Retrieved {Count} officers for company {CompanyNumber}",
|
||||
result?.Items?.Count ?? 0, companyNumber);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.LogWarning("Rate limit exceeded for Companies House API");
|
||||
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DTOs for Companies House API responses
|
||||
public sealed record CompaniesHouseSearchResponse
|
||||
{
|
||||
public int TotalResults { get; init; }
|
||||
public int ItemsPerPage { get; init; }
|
||||
public int StartIndex { get; init; }
|
||||
public List<CompaniesHouseSearchItem> Items { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record CompaniesHouseSearchItem
|
||||
{
|
||||
public required string CompanyNumber { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public string? CompanyStatus { get; init; }
|
||||
public string? CompanyType { get; init; }
|
||||
public string? DateOfCreation { get; init; }
|
||||
public string? DateOfCessation { get; init; }
|
||||
public CompaniesHouseAddress? Address { get; init; }
|
||||
public string? AddressSnippet { get; init; }
|
||||
public List<string>? SicCodes { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CompaniesHouseCompany
|
||||
{
|
||||
public required string CompanyNumber { get; init; }
|
||||
public required string CompanyName { get; init; }
|
||||
public string? CompanyStatus { get; init; }
|
||||
public string? Type { get; init; }
|
||||
public string? DateOfCreation { get; init; }
|
||||
public string? DateOfCessation { get; init; }
|
||||
public CompaniesHouseAddress? RegisteredOfficeAddress { get; init; }
|
||||
public List<string>? SicCodes { get; init; }
|
||||
public CompaniesHouseAccounts? Accounts { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CompaniesHouseAccounts
|
||||
{
|
||||
public CompaniesHouseAccountingReferenceDate? AccountingReferenceDate { get; init; }
|
||||
public CompaniesHouseLastAccounts? LastAccounts { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CompaniesHouseAccountingReferenceDate
|
||||
{
|
||||
public string? Day { get; init; }
|
||||
public string? Month { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CompaniesHouseLastAccounts
|
||||
{
|
||||
public string? MadeUpTo { get; init; }
|
||||
public string? Type { get; init; } // e.g., "micro-entity", "small", "medium", "dormant", etc.
|
||||
}
|
||||
|
||||
public sealed record CompaniesHouseOfficersResponse
|
||||
{
|
||||
public int TotalResults { get; init; }
|
||||
public List<CompaniesHouseOfficer> Items { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record CompaniesHouseOfficer
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public string? OfficerRole { get; init; }
|
||||
public string? AppointedOn { get; init; }
|
||||
public string? ResignedOn { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CompaniesHouseAddress
|
||||
{
|
||||
public string? Premises { get; init; }
|
||||
public string? AddressLine1 { get; init; }
|
||||
public string? AddressLine2 { get; init; }
|
||||
public string? Locality { get; init; }
|
||||
public string? Region { get; init; }
|
||||
public string? PostalCode { get; init; }
|
||||
public string? Country { get; init; }
|
||||
}
|
||||
|
||||
public class CompaniesHouseRateLimitException : Exception
|
||||
{
|
||||
public CompaniesHouseRateLimitException(string message) : base(message) { }
|
||||
public CompaniesHouseRateLimitException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
32
src/RealCV.Infrastructure/Helpers/JsonResponseHelper.cs
Normal file
32
src/RealCV.Infrastructure/Helpers/JsonResponseHelper.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
namespace RealCV.Infrastructure.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for processing AI/LLM JSON responses.
|
||||
/// </summary>
|
||||
public static class JsonResponseHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Cleans a JSON response by removing markdown code block formatting.
|
||||
/// </summary>
|
||||
public static string CleanJsonResponse(string response)
|
||||
{
|
||||
var trimmed = response.Trim();
|
||||
|
||||
// Remove markdown code blocks
|
||||
if (trimmed.StartsWith("```json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
trimmed = trimmed[7..];
|
||||
}
|
||||
else if (trimmed.StartsWith("```"))
|
||||
{
|
||||
trimmed = trimmed[3..];
|
||||
}
|
||||
|
||||
if (trimmed.EndsWith("```"))
|
||||
{
|
||||
trimmed = trimmed[..^3];
|
||||
}
|
||||
|
||||
return trimmed.Trim();
|
||||
}
|
||||
}
|
||||
16
src/RealCV.Infrastructure/Identity/ApplicationUser.cs
Normal file
16
src/RealCV.Infrastructure/Identity/ApplicationUser.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Domain.Enums;
|
||||
|
||||
namespace RealCV.Infrastructure.Identity;
|
||||
|
||||
public class ApplicationUser : IdentityUser<Guid>
|
||||
{
|
||||
public UserPlan Plan { get; set; }
|
||||
|
||||
public string? StripeCustomerId { get; set; }
|
||||
|
||||
public int ChecksUsedThisMonth { get; set; }
|
||||
|
||||
public ICollection<CVCheck> CVChecks { get; set; } = new List<CVCheck>();
|
||||
}
|
||||
1385
src/RealCV.Infrastructure/Jobs/ProcessCVCheckJob.cs
Normal file
1385
src/RealCV.Infrastructure/Jobs/ProcessCVCheckJob.cs
Normal file
File diff suppressed because it is too large
Load Diff
31
src/RealCV.Infrastructure/RealCV.Infrastructure.csproj
Normal file
31
src/RealCV.Infrastructure/RealCV.Infrastructure.csproj
Normal file
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RealCV.Application\RealCV.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Anthropic.SDK" Version="5.8.0" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.27.0" />
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.4.1" />
|
||||
<PackageReference Include="FuzzySharp" Version="2.0.2" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.*" />
|
||||
<PackageReference Include="Hangfire.SqlServer" Version="1.8.*" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.*">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.*" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="UglyToad.PdfPig" Version="1.7.0-custom-5" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Text.Json;
|
||||
using Anthropic.SDK;
|
||||
using Anthropic.SDK.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RealCV.Application.Helpers;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
using RealCV.Infrastructure.Helpers;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class AICompanyNameMatcherService : ICompanyNameMatcherService
|
||||
{
|
||||
private readonly AnthropicClient _anthropicClient;
|
||||
private readonly ILogger<AICompanyNameMatcherService> _logger;
|
||||
|
||||
private const string SystemPrompt = """
|
||||
You are a UK company name matching expert. Your task is to determine if a company name
|
||||
from a CV matches any of the official company names from Companies House records.
|
||||
|
||||
You understand:
|
||||
- Trading names vs registered names (e.g., "Tesco" = "TESCO PLC")
|
||||
- Subsidiaries vs parent companies (e.g., "ASDA" might work for "ASDA STORES LIMITED")
|
||||
- Common abbreviations (Ltd = Limited, PLC = Public Limited Company, CiC = Community Interest Company)
|
||||
- That completely different words mean different companies (e.g., "Families First" ≠ "Families Against Conformity")
|
||||
|
||||
You must respond ONLY with valid JSON, no other text or markdown.
|
||||
""";
|
||||
|
||||
private const string MatchingPrompt = """
|
||||
Compare the company name from a CV against official Companies House records.
|
||||
|
||||
CV Company Name: "{CV_COMPANY}"
|
||||
|
||||
Companies House Candidates:
|
||||
{CANDIDATES}
|
||||
|
||||
Determine which candidate (if any) is the SAME company as the CV entry.
|
||||
|
||||
Rules:
|
||||
1. A match requires the companies to be the SAME organisation, not just similar names
|
||||
2. "Families First CiC" is NOT the same as "FAMILIES AGAINST CONFORMITY LTD" - different words = different companies
|
||||
3. Trading names should match their registered entity (e.g., "Tesco" matches "TESCO PLC")
|
||||
4. Subsidiaries can match if clearly the same organisation (e.g., "ASDA" could match "ASDA STORES LIMITED")
|
||||
5. Acronyms in parentheses are abbreviations of the full name (e.g., "North Halifax Partnership (NHP)" = "NORTH HALIFAX PARTNERSHIP")
|
||||
6. CiC/CIC = Community Interest Company, LLP = Limited Liability Partnership - these are legal suffixes
|
||||
7. If the CV name contains all the key words of a candidate (ignoring Ltd/Limited/CIC/etc.), it's likely a match
|
||||
8. If NO candidate is clearly the same company, return "NONE" as the best match
|
||||
|
||||
Respond with this exact JSON structure:
|
||||
{
|
||||
"bestMatchCompanyNumber": "string (company number of best match, or 'NONE' if no valid match)",
|
||||
"confidenceScore": number (0-100, where 100 = certain match, 0 = no match),
|
||||
"matchType": "string (Exact, TradingName, Subsidiary, Parent, NoMatch)",
|
||||
"reasoning": "string (brief explanation of why this is or isn't a match)"
|
||||
}
|
||||
""";
|
||||
|
||||
public AICompanyNameMatcherService(
|
||||
IOptions<AnthropicSettings> settings,
|
||||
ILogger<AICompanyNameMatcherService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_anthropicClient = new AnthropicClient(settings.Value.ApiKey);
|
||||
}
|
||||
|
||||
public async Task<SemanticMatchResult?> FindBestMatchAsync(
|
||||
string cvCompanyName,
|
||||
List<CompanyCandidate> candidates,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cvCompanyName) || candidates.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Using AI to match '{CVCompany}' against {Count} candidates",
|
||||
cvCompanyName, candidates.Count);
|
||||
|
||||
try
|
||||
{
|
||||
var candidatesText = string.Join("\n", candidates.Select((c, i) =>
|
||||
$"{i + 1}. {c.CompanyName} (Number: {c.CompanyNumber}, Status: {c.CompanyStatus ?? "Unknown"})"));
|
||||
|
||||
var prompt = MatchingPrompt
|
||||
.Replace("{CV_COMPANY}", cvCompanyName)
|
||||
.Replace("{CANDIDATES}", candidatesText);
|
||||
|
||||
var messages = new List<Message>
|
||||
{
|
||||
new(RoleType.User, prompt)
|
||||
};
|
||||
|
||||
var parameters = new MessageParameters
|
||||
{
|
||||
Model = "claude-sonnet-4-20250514",
|
||||
MaxTokens = 1024,
|
||||
Messages = messages,
|
||||
System = [new SystemMessage(SystemPrompt)]
|
||||
};
|
||||
|
||||
var response = await _anthropicClient.Messages.GetClaudeMessageAsync(parameters, cancellationToken);
|
||||
|
||||
var responseText = response.Content
|
||||
.OfType<TextContent>()
|
||||
.FirstOrDefault()?.Text;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(responseText))
|
||||
{
|
||||
_logger.LogWarning("AI returned empty response for company matching");
|
||||
return null;
|
||||
}
|
||||
|
||||
responseText = JsonResponseHelper.CleanJsonResponse(responseText);
|
||||
|
||||
var aiResponse = JsonSerializer.Deserialize<AIMatchResponse>(responseText, JsonDefaults.CamelCase);
|
||||
|
||||
if (aiResponse is null)
|
||||
{
|
||||
_logger.LogWarning("Failed to deserialize AI response: {Response}", responseText);
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogDebug("AI match result: {CompanyNumber} with {Score}% confidence - {Reasoning}",
|
||||
aiResponse.BestMatchCompanyNumber, aiResponse.ConfidenceScore, aiResponse.Reasoning);
|
||||
|
||||
// Find the matched candidate
|
||||
if (aiResponse.BestMatchCompanyNumber == "NONE" || aiResponse.ConfidenceScore < 50)
|
||||
{
|
||||
return new SemanticMatchResult
|
||||
{
|
||||
CandidateCompanyName = "No match",
|
||||
CandidateCompanyNumber = "NONE",
|
||||
ConfidenceScore = 0,
|
||||
MatchType = "NoMatch",
|
||||
Reasoning = aiResponse.Reasoning
|
||||
};
|
||||
}
|
||||
|
||||
var matchedCandidate = candidates.FirstOrDefault(c =>
|
||||
c.CompanyNumber.Equals(aiResponse.BestMatchCompanyNumber, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (matchedCandidate is null)
|
||||
{
|
||||
_logger.LogWarning("AI returned company number {Number} not in candidates list",
|
||||
aiResponse.BestMatchCompanyNumber);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SemanticMatchResult
|
||||
{
|
||||
CandidateCompanyName = matchedCandidate.CompanyName,
|
||||
CandidateCompanyNumber = matchedCandidate.CompanyNumber,
|
||||
ConfidenceScore = aiResponse.ConfidenceScore,
|
||||
MatchType = aiResponse.MatchType,
|
||||
Reasoning = aiResponse.Reasoning
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "AI company matching failed for '{CVCompany}'", cvCompanyName);
|
||||
return null; // Fall back to fuzzy matching
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/RealCV.Infrastructure/Services/AuditService.cs
Normal file
52
src/RealCV.Infrastructure/Services/AuditService.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Infrastructure.Data;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class AuditService : IAuditService
|
||||
{
|
||||
private readonly ApplicationDbContext _dbContext;
|
||||
private readonly ILogger<AuditService> _logger;
|
||||
|
||||
public AuditService(ApplicationDbContext dbContext, ILogger<AuditService> logger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task LogAsync(
|
||||
Guid userId,
|
||||
string action,
|
||||
string? entityType = null,
|
||||
Guid? entityId = null,
|
||||
string? details = null,
|
||||
string? ipAddress = null)
|
||||
{
|
||||
var auditLog = new AuditLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Action = action,
|
||||
EntityType = entityType,
|
||||
EntityId = entityId,
|
||||
Details = details,
|
||||
IpAddress = ipAddress,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_dbContext.AuditLogs.Add(auditLog);
|
||||
|
||||
try
|
||||
{
|
||||
await _dbContext.SaveChangesAsync();
|
||||
_logger.LogDebug("Audit log created: {Action} by user {UserId}", action, userId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Don't let audit failures break the main flow
|
||||
_logger.LogError(ex, "Failed to create audit log: {Action} by user {UserId}", action, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
198
src/RealCV.Infrastructure/Services/CVCheckService.cs
Normal file
198
src/RealCV.Infrastructure/Services/CVCheckService.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
using System.Text.Json;
|
||||
using Hangfire;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RealCV.Application.DTOs;
|
||||
using RealCV.Application.Helpers;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Domain.Enums;
|
||||
using RealCV.Infrastructure.Data;
|
||||
using RealCV.Infrastructure.Jobs;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class CVCheckService : ICVCheckService
|
||||
{
|
||||
private readonly ApplicationDbContext _dbContext;
|
||||
private readonly IFileStorageService _fileStorageService;
|
||||
private readonly IBackgroundJobClient _backgroundJobClient;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly ILogger<CVCheckService> _logger;
|
||||
|
||||
public CVCheckService(
|
||||
ApplicationDbContext dbContext,
|
||||
IFileStorageService fileStorageService,
|
||||
IBackgroundJobClient backgroundJobClient,
|
||||
IAuditService auditService,
|
||||
ILogger<CVCheckService> logger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_fileStorageService = fileStorageService;
|
||||
_backgroundJobClient = backgroundJobClient;
|
||||
_auditService = auditService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateCheckAsync(Guid userId, Stream file, string fileName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(file);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
|
||||
|
||||
_logger.LogDebug("Creating CV check for user {UserId}, file: {FileName}", userId, fileName);
|
||||
|
||||
// Upload file to blob storage
|
||||
var blobUrl = await _fileStorageService.UploadAsync(file, fileName);
|
||||
|
||||
_logger.LogDebug("File uploaded to: {BlobUrl}", blobUrl);
|
||||
|
||||
// Create CV check record
|
||||
var cvCheck = new CVCheck
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
OriginalFileName = fileName,
|
||||
BlobUrl = blobUrl,
|
||||
Status = CheckStatus.Pending
|
||||
};
|
||||
|
||||
_dbContext.CVChecks.Add(cvCheck);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogDebug("CV check record created with ID: {CheckId}", cvCheck.Id);
|
||||
|
||||
// Queue background job for processing
|
||||
_backgroundJobClient.Enqueue<ProcessCVCheckJob>(job => job.ExecuteAsync(cvCheck.Id, CancellationToken.None));
|
||||
|
||||
_logger.LogInformation(
|
||||
"CV check {CheckId} created for user {UserId}, processing queued",
|
||||
cvCheck.Id, userId);
|
||||
|
||||
await _auditService.LogAsync(userId, AuditActions.CVUploaded, "CVCheck", cvCheck.Id, $"File: {fileName}");
|
||||
|
||||
return cvCheck.Id;
|
||||
}
|
||||
|
||||
public async Task<CVCheckDto?> GetCheckAsync(Guid id)
|
||||
{
|
||||
_logger.LogDebug("Retrieving CV check: {CheckId}", id);
|
||||
|
||||
var cvCheck = await _dbContext.CVChecks
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.Id == id);
|
||||
|
||||
if (cvCheck is null)
|
||||
{
|
||||
_logger.LogDebug("CV check not found: {CheckId}", id);
|
||||
return null;
|
||||
}
|
||||
|
||||
return MapToDto(cvCheck);
|
||||
}
|
||||
|
||||
public async Task<List<CVCheckDto>> GetUserChecksAsync(Guid userId)
|
||||
{
|
||||
_logger.LogDebug("Retrieving CV checks for user: {UserId}", userId);
|
||||
|
||||
var checks = await _dbContext.CVChecks
|
||||
.AsNoTracking()
|
||||
.Where(c => c.UserId == userId)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogDebug("Found {Count} CV checks for user {UserId}", checks.Count, userId);
|
||||
|
||||
return checks.Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<CVCheckDto?> GetCheckForUserAsync(Guid id, Guid userId)
|
||||
{
|
||||
_logger.LogDebug("Retrieving CV check {CheckId} for user {UserId}", id, userId);
|
||||
|
||||
var cvCheck = await _dbContext.CVChecks
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId);
|
||||
|
||||
if (cvCheck is null)
|
||||
{
|
||||
_logger.LogDebug("CV check not found: {CheckId} for user {UserId}", id, userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return MapToDto(cvCheck);
|
||||
}
|
||||
|
||||
public async Task<VeracityReport?> GetReportAsync(Guid checkId, Guid userId)
|
||||
{
|
||||
_logger.LogDebug("Retrieving report for CV check {CheckId}, user {UserId}", checkId, userId);
|
||||
|
||||
var cvCheck = await _dbContext.CVChecks
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.Id == checkId && c.UserId == userId);
|
||||
|
||||
if (cvCheck is null)
|
||||
{
|
||||
_logger.LogWarning("CV check not found: {CheckId} for user {UserId}", checkId, userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cvCheck.Status != CheckStatus.Completed || string.IsNullOrEmpty(cvCheck.ReportJson))
|
||||
{
|
||||
_logger.LogDebug("CV check {CheckId} not completed or has no report", checkId);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var report = JsonSerializer.Deserialize<VeracityReport>(cvCheck.ReportJson, JsonDefaults.CamelCase);
|
||||
return report;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to deserialize report JSON for check {CheckId}", checkId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteCheckAsync(Guid checkId, Guid userId)
|
||||
{
|
||||
_logger.LogDebug("Deleting CV check {CheckId} for user {UserId}", checkId, userId);
|
||||
|
||||
var cvCheck = await _dbContext.CVChecks
|
||||
.Include(c => c.Flags)
|
||||
.FirstOrDefaultAsync(c => c.Id == checkId && c.UserId == userId);
|
||||
|
||||
if (cvCheck is null)
|
||||
{
|
||||
_logger.LogWarning("CV check {CheckId} not found for user {UserId}", checkId, userId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var fileName = cvCheck.OriginalFileName;
|
||||
|
||||
_dbContext.CVFlags.RemoveRange(cvCheck.Flags);
|
||||
_dbContext.CVChecks.Remove(cvCheck);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Deleted CV check {CheckId} for user {UserId}", checkId, userId);
|
||||
|
||||
await _auditService.LogAsync(userId, AuditActions.CVDeleted, "CVCheck", checkId, $"File: {fileName}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static CVCheckDto MapToDto(CVCheck cvCheck)
|
||||
{
|
||||
return new CVCheckDto
|
||||
{
|
||||
Id = cvCheck.Id,
|
||||
OriginalFileName = cvCheck.OriginalFileName,
|
||||
Status = cvCheck.Status.ToString(),
|
||||
VeracityScore = cvCheck.VeracityScore,
|
||||
ProcessingStage = cvCheck.ProcessingStage,
|
||||
CreatedAt = cvCheck.CreatedAt,
|
||||
CompletedAt = cvCheck.CompletedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
398
src/RealCV.Infrastructure/Services/CVParserService.cs
Normal file
398
src/RealCV.Infrastructure/Services/CVParserService.cs
Normal file
@@ -0,0 +1,398 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Anthropic.SDK;
|
||||
using Anthropic.SDK.Messaging;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RealCV.Application.Helpers;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
using RealCV.Infrastructure.Helpers;
|
||||
using UglyToad.PdfPig;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class CVParserService : ICVParserService
|
||||
{
|
||||
private readonly AnthropicClient _anthropicClient;
|
||||
private readonly ILogger<CVParserService> _logger;
|
||||
|
||||
private const string SystemPrompt = """
|
||||
You are a CV/Resume parser. Your task is to extract structured information from CV text.
|
||||
You must respond ONLY with valid JSON, no other text or markdown.
|
||||
""";
|
||||
|
||||
private const string ExtractionPrompt = """
|
||||
Parse the following CV text and extract the information into this exact JSON structure:
|
||||
|
||||
{
|
||||
"fullName": "string (required)",
|
||||
"email": "string or null",
|
||||
"phone": "string or null",
|
||||
"employment": [
|
||||
{
|
||||
"companyName": "string (required)",
|
||||
"jobTitle": "string (required)",
|
||||
"location": "string or null",
|
||||
"startDate": "YYYY-MM-DD or null",
|
||||
"endDate": "YYYY-MM-DD or null (null if current)",
|
||||
"isCurrent": "boolean",
|
||||
"description": "string or null"
|
||||
}
|
||||
],
|
||||
"education": [
|
||||
{
|
||||
"institution": "string (required)",
|
||||
"qualification": "string or null (e.g., BSc, MSc, PhD)",
|
||||
"subject": "string or null",
|
||||
"grade": "string or null",
|
||||
"startDate": "YYYY-MM-DD or null",
|
||||
"endDate": "YYYY-MM-DD or null"
|
||||
}
|
||||
],
|
||||
"skills": ["array of skill strings"]
|
||||
}
|
||||
|
||||
Rules:
|
||||
- For dates, use the first day of the month if only month/year is given (e.g., "Jan 2020" becomes "2020-01-01")
|
||||
- For dates with only year, use January 1st (e.g., "2020" becomes "2020-01-01")
|
||||
- Set isCurrent to true if the job appears to be ongoing (e.g., "Present", "Current", no end date mentioned with recent start)
|
||||
- Extract all employment history in chronological order
|
||||
- If information is not available, use null
|
||||
- Do not invent or assume information not present in the text
|
||||
|
||||
CV TEXT:
|
||||
{CV_TEXT}
|
||||
""";
|
||||
|
||||
public CVParserService(
|
||||
IOptions<AnthropicSettings> settings,
|
||||
ILogger<CVParserService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_anthropicClient = new AnthropicClient(settings.Value.ApiKey);
|
||||
}
|
||||
|
||||
public async Task<CVData> ParseAsync(Stream fileStream, string fileName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fileStream);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
|
||||
|
||||
_logger.LogDebug("Parsing CV file: {FileName}", fileName);
|
||||
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
|
||||
// Handle JSON files directly (debug/test format)
|
||||
if (extension == ".json")
|
||||
{
|
||||
var cvData = await ParseJsonFileAsync(fileStream, cancellationToken);
|
||||
_logger.LogInformation(
|
||||
"Successfully loaded JSON CV for {FullName} with {EmploymentCount} employment entries and {EducationCount} education entries",
|
||||
cvData.FullName,
|
||||
cvData.Employment.Count,
|
||||
cvData.Education.Count);
|
||||
return cvData;
|
||||
}
|
||||
|
||||
var text = await ExtractTextAsync(fileStream, fileName, cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
_logger.LogWarning("No text content extracted from file: {FileName}", fileName);
|
||||
throw new InvalidOperationException($"Could not extract text content from file: {fileName}");
|
||||
}
|
||||
|
||||
_logger.LogDebug("Extracted {CharCount} characters from {FileName}", text.Length, fileName);
|
||||
|
||||
var cvDataFromAI = await ParseWithClaudeAsync(text, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Successfully parsed CV for {FullName} with {EmploymentCount} employment entries and {EducationCount} education entries",
|
||||
cvDataFromAI.FullName,
|
||||
cvDataFromAI.Employment.Count,
|
||||
cvDataFromAI.Education.Count);
|
||||
|
||||
return cvDataFromAI;
|
||||
}
|
||||
|
||||
private async Task<string> ExtractTextAsync(Stream fileStream, string fileName, CancellationToken cancellationToken)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
|
||||
return extension switch
|
||||
{
|
||||
".pdf" => await ExtractTextFromPdfAsync(fileStream, cancellationToken),
|
||||
".docx" => ExtractTextFromDocx(fileStream),
|
||||
_ => throw new NotSupportedException($"File type '{extension}' is not supported. Only PDF and DOCX files are accepted.")
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<CVData> ParseJsonFileAsync(Stream fileStream, CancellationToken cancellationToken)
|
||||
{
|
||||
var testCv = await JsonSerializer.DeserializeAsync<TestCVData>(fileStream, TestJsonOptions, cancellationToken)
|
||||
?? throw new InvalidOperationException("Failed to deserialize JSON CV file");
|
||||
|
||||
return new CVData
|
||||
{
|
||||
FullName = testCv.Personal?.Name ?? "Unknown",
|
||||
Email = testCv.Personal?.Email,
|
||||
Phone = testCv.Personal?.Phone,
|
||||
Employment = testCv.Employment?.Select(e => new EmploymentEntry
|
||||
{
|
||||
CompanyName = e.Company ?? "Unknown",
|
||||
JobTitle = e.JobTitle ?? "Unknown",
|
||||
Location = e.Location,
|
||||
StartDate = ParseTestDate(e.StartDate),
|
||||
EndDate = ParseTestDate(e.EndDate),
|
||||
IsCurrent = e.EndDate == null,
|
||||
Description = e.Description
|
||||
}).ToList() ?? [],
|
||||
Education = testCv.Education?.Select(e => new EducationEntry
|
||||
{
|
||||
Institution = e.Institution ?? "Unknown",
|
||||
Qualification = e.Qualification,
|
||||
Subject = e.Subject,
|
||||
StartDate = ParseTestDate(e.StartDate),
|
||||
EndDate = ParseTestDate(e.EndDate)
|
||||
}).ToList() ?? [],
|
||||
Skills = testCv.Skills ?? []
|
||||
};
|
||||
}
|
||||
|
||||
private static DateOnly? ParseTestDate(string? dateStr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dateStr)) return null;
|
||||
|
||||
// Try parsing YYYY-MM format
|
||||
if (dateStr.Length == 7 && dateStr[4] == '-')
|
||||
{
|
||||
if (int.TryParse(dateStr[..4], out var year) && int.TryParse(dateStr[5..], out var month))
|
||||
{
|
||||
return new DateOnly(year, month, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Try standard parsing
|
||||
if (DateOnly.TryParse(dateStr, out var date))
|
||||
{
|
||||
return date;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<string> ExtractTextFromPdfAsync(Stream fileStream, CancellationToken cancellationToken)
|
||||
{
|
||||
// Copy stream to memory for PdfPig (requires seekable stream)
|
||||
using var memoryStream = new MemoryStream();
|
||||
await fileStream.CopyToAsync(memoryStream, cancellationToken);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var document = PdfDocument.Open(memoryStream);
|
||||
var textBuilder = new StringBuilder();
|
||||
|
||||
foreach (var page in document.GetPages())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var pageText = page.Text;
|
||||
textBuilder.AppendLine(pageText);
|
||||
}
|
||||
|
||||
return textBuilder.ToString();
|
||||
}
|
||||
|
||||
private static string ExtractTextFromDocx(Stream fileStream)
|
||||
{
|
||||
using var document = WordprocessingDocument.Open(fileStream, false);
|
||||
var body = document.MainDocumentPart?.Document?.Body;
|
||||
|
||||
if (body is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var textBuilder = new StringBuilder();
|
||||
|
||||
foreach (var paragraph in body.Elements<Paragraph>())
|
||||
{
|
||||
var paragraphText = paragraph.InnerText;
|
||||
if (!string.IsNullOrWhiteSpace(paragraphText))
|
||||
{
|
||||
textBuilder.AppendLine(paragraphText);
|
||||
}
|
||||
}
|
||||
|
||||
return textBuilder.ToString();
|
||||
}
|
||||
|
||||
private async Task<CVData> ParseWithClaudeAsync(string cvText, CancellationToken cancellationToken)
|
||||
{
|
||||
var prompt = ExtractionPrompt.Replace("{CV_TEXT}", cvText);
|
||||
|
||||
var messages = new List<Message>
|
||||
{
|
||||
new(RoleType.User, prompt)
|
||||
};
|
||||
|
||||
var parameters = new MessageParameters
|
||||
{
|
||||
Model = "claude-sonnet-4-20250514",
|
||||
MaxTokens = 4096,
|
||||
Messages = messages,
|
||||
System = [new SystemMessage(SystemPrompt)]
|
||||
};
|
||||
|
||||
_logger.LogDebug("Sending CV text to Claude API for parsing");
|
||||
|
||||
var response = await _anthropicClient.Messages.GetClaudeMessageAsync(parameters, cancellationToken);
|
||||
|
||||
var responseText = response.Content
|
||||
.OfType<TextContent>()
|
||||
.FirstOrDefault()?.Text;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(responseText))
|
||||
{
|
||||
_logger.LogError("Claude API returned empty response");
|
||||
throw new InvalidOperationException("Failed to parse CV: AI returned empty response");
|
||||
}
|
||||
|
||||
// Clean up response - remove markdown code blocks if present
|
||||
responseText = JsonResponseHelper.CleanJsonResponse(responseText);
|
||||
|
||||
_logger.LogDebug("Received response from Claude API, parsing JSON");
|
||||
|
||||
try
|
||||
{
|
||||
var parsedResponse = JsonSerializer.Deserialize<ClaudeCVResponse>(responseText, JsonDefaults.CamelCase);
|
||||
|
||||
if (parsedResponse is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize CV data from AI response");
|
||||
}
|
||||
|
||||
return MapToCVData(parsedResponse);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse Claude response as JSON: {Response}", responseText);
|
||||
throw new InvalidOperationException("Failed to parse CV: AI returned invalid JSON", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static CVData MapToCVData(ClaudeCVResponse response)
|
||||
{
|
||||
return new CVData
|
||||
{
|
||||
FullName = response.FullName ?? "Unknown",
|
||||
Email = response.Email,
|
||||
Phone = response.Phone,
|
||||
Employment = response.Employment?.Select(e => new EmploymentEntry
|
||||
{
|
||||
CompanyName = e.CompanyName ?? "Unknown Company",
|
||||
JobTitle = e.JobTitle ?? "Unknown Position",
|
||||
Location = e.Location,
|
||||
StartDate = DateHelpers.ParseDate(e.StartDate),
|
||||
EndDate = DateHelpers.ParseDate(e.EndDate),
|
||||
IsCurrent = e.IsCurrent ?? false,
|
||||
Description = e.Description
|
||||
}).ToList() ?? [],
|
||||
Education = response.Education?.Select(e => new EducationEntry
|
||||
{
|
||||
Institution = e.Institution ?? "Unknown Institution",
|
||||
Qualification = e.Qualification,
|
||||
Subject = e.Subject,
|
||||
Grade = e.Grade,
|
||||
StartDate = DateHelpers.ParseDate(e.StartDate),
|
||||
EndDate = DateHelpers.ParseDate(e.EndDate)
|
||||
}).ToList() ?? [],
|
||||
Skills = response.Skills ?? []
|
||||
};
|
||||
}
|
||||
|
||||
// JSON options for test/debug CV format (snake_case)
|
||||
private static readonly JsonSerializerOptions TestJsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
// DTOs for test JSON format (snake_case with nested personal object)
|
||||
private sealed record TestCVData
|
||||
{
|
||||
public string? CvId { get; init; }
|
||||
public string? Category { get; init; }
|
||||
public List<string>? ExpectedFlags { get; init; }
|
||||
public TestPersonalData? Personal { get; init; }
|
||||
public string? Profile { get; init; }
|
||||
public List<TestEmploymentEntry>? Employment { get; init; }
|
||||
public List<TestEducationEntry>? Education { get; init; }
|
||||
public List<string>? Skills { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestPersonalData
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
public string? Email { get; init; }
|
||||
public string? Phone { get; init; }
|
||||
public string? Address { get; init; }
|
||||
public string? LinkedIn { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestEmploymentEntry
|
||||
{
|
||||
public string? Company { get; init; }
|
||||
public string? JobTitle { get; init; }
|
||||
public string? StartDate { get; init; }
|
||||
public string? EndDate { get; init; }
|
||||
public string? Location { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public List<string>? Achievements { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestEducationEntry
|
||||
{
|
||||
public string? Institution { get; init; }
|
||||
public string? Qualification { get; init; }
|
||||
public string? Subject { get; init; }
|
||||
public string? Classification { get; init; }
|
||||
public string? StartDate { get; init; }
|
||||
public string? EndDate { get; init; }
|
||||
}
|
||||
|
||||
// Internal DTOs for Claude response parsing
|
||||
private sealed record ClaudeCVResponse
|
||||
{
|
||||
public string? FullName { get; init; }
|
||||
public string? Email { get; init; }
|
||||
public string? Phone { get; init; }
|
||||
public List<ClaudeEmploymentEntry>? Employment { get; init; }
|
||||
public List<ClaudeEducationEntry>? Education { get; init; }
|
||||
public List<string>? Skills { get; init; }
|
||||
}
|
||||
|
||||
private sealed record ClaudeEmploymentEntry
|
||||
{
|
||||
public string? CompanyName { get; init; }
|
||||
public string? JobTitle { get; init; }
|
||||
public string? Location { get; init; }
|
||||
public string? StartDate { get; init; }
|
||||
public string? EndDate { get; init; }
|
||||
public bool? IsCurrent { get; init; }
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
private sealed record ClaudeEducationEntry
|
||||
{
|
||||
public string? Institution { get; init; }
|
||||
public string? Qualification { get; init; }
|
||||
public string? Subject { get; init; }
|
||||
public string? Grade { get; init; }
|
||||
public string? StartDate { get; init; }
|
||||
public string? EndDate { get; init; }
|
||||
}
|
||||
}
|
||||
1264
src/RealCV.Infrastructure/Services/CompanyVerifierService.cs
Normal file
1264
src/RealCV.Infrastructure/Services/CompanyVerifierService.cs
Normal file
File diff suppressed because it is too large
Load Diff
271
src/RealCV.Infrastructure/Services/EducationVerifierService.cs
Normal file
271
src/RealCV.Infrastructure/Services/EducationVerifierService.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
using RealCV.Application.Data;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class EducationVerifierService : IEducationVerifierService
|
||||
{
|
||||
private const int MinimumDegreeYears = 1;
|
||||
private const int MaximumDegreeYears = 8;
|
||||
private const int MinimumGraduationAge = 18;
|
||||
|
||||
public EducationVerificationResult Verify(EducationEntry education)
|
||||
{
|
||||
var institution = education.Institution;
|
||||
|
||||
// Check for diploma mill first (highest priority flag)
|
||||
if (DiplomaMills.IsDiplomaMill(institution))
|
||||
{
|
||||
return new EducationVerificationResult
|
||||
{
|
||||
ClaimedInstitution = institution,
|
||||
Status = "DiplomaMill",
|
||||
IsVerified = false,
|
||||
IsDiplomaMill = true,
|
||||
IsSuspicious = true,
|
||||
VerificationNotes = "Institution is on the diploma mill blacklist",
|
||||
ClaimedStartDate = education.StartDate,
|
||||
ClaimedEndDate = education.EndDate,
|
||||
DatesArePlausible = true,
|
||||
ClaimedQualification = education.Qualification,
|
||||
ClaimedSubject = education.Subject
|
||||
};
|
||||
}
|
||||
|
||||
// Check for suspicious patterns
|
||||
if (DiplomaMills.HasSuspiciousPattern(institution))
|
||||
{
|
||||
return new EducationVerificationResult
|
||||
{
|
||||
ClaimedInstitution = institution,
|
||||
Status = "Suspicious",
|
||||
IsVerified = false,
|
||||
IsDiplomaMill = false,
|
||||
IsSuspicious = true,
|
||||
VerificationNotes = "Institution name contains suspicious patterns common in diploma mills",
|
||||
ClaimedStartDate = education.StartDate,
|
||||
ClaimedEndDate = education.EndDate,
|
||||
DatesArePlausible = true,
|
||||
ClaimedQualification = education.Qualification,
|
||||
ClaimedSubject = education.Subject
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's a recognised UK institution
|
||||
var officialName = UKInstitutions.GetOfficialName(institution);
|
||||
if (officialName != null)
|
||||
{
|
||||
var (datesPlausible, dateNotes) = CheckDatePlausibility(education.StartDate, education.EndDate);
|
||||
|
||||
return new EducationVerificationResult
|
||||
{
|
||||
ClaimedInstitution = institution,
|
||||
MatchedInstitution = officialName,
|
||||
Status = "Recognised",
|
||||
IsVerified = true,
|
||||
IsDiplomaMill = false,
|
||||
IsSuspicious = false,
|
||||
VerificationNotes = institution.Equals(officialName, StringComparison.OrdinalIgnoreCase)
|
||||
? "Verified UK higher education institution"
|
||||
: $"Matched to official name: {officialName}",
|
||||
ClaimedStartDate = education.StartDate,
|
||||
ClaimedEndDate = education.EndDate,
|
||||
DatesArePlausible = datesPlausible,
|
||||
DatePlausibilityNotes = dateNotes,
|
||||
ClaimedQualification = education.Qualification,
|
||||
ClaimedSubject = education.Subject
|
||||
};
|
||||
}
|
||||
|
||||
// Not in our database - could be international or unrecognised
|
||||
var isUnknownInstitution = string.IsNullOrWhiteSpace(institution) ||
|
||||
institution.Equals("Unknown Institution", StringComparison.OrdinalIgnoreCase) ||
|
||||
institution.Equals("Unknown", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return new EducationVerificationResult
|
||||
{
|
||||
ClaimedInstitution = institution,
|
||||
Status = "Unknown",
|
||||
IsVerified = false,
|
||||
IsDiplomaMill = false,
|
||||
IsSuspicious = false,
|
||||
VerificationNotes = isUnknownInstitution ? null : "Institution not found in UK recognised institutions database. May be an international institution.",
|
||||
ClaimedStartDate = education.StartDate,
|
||||
ClaimedEndDate = education.EndDate,
|
||||
DatesArePlausible = true,
|
||||
ClaimedQualification = education.Qualification,
|
||||
ClaimedSubject = education.Subject
|
||||
};
|
||||
}
|
||||
|
||||
public List<EducationVerificationResult> VerifyAll(
|
||||
List<EducationEntry> education,
|
||||
List<EmploymentEntry>? employment = null)
|
||||
{
|
||||
var results = new List<EducationVerificationResult>();
|
||||
|
||||
foreach (var edu in education)
|
||||
{
|
||||
var result = Verify(edu);
|
||||
|
||||
// If we have employment data, check for timeline issues
|
||||
if (employment?.Count > 0 && result.ClaimedEndDate.HasValue)
|
||||
{
|
||||
var (timelinePlausible, timelineNotes) = CheckEducationEmploymentTimeline(
|
||||
result.ClaimedEndDate.Value,
|
||||
employment);
|
||||
|
||||
if (!timelinePlausible)
|
||||
{
|
||||
result = result with
|
||||
{
|
||||
DatesArePlausible = false,
|
||||
DatePlausibilityNotes = CombineNotes(result.DatePlausibilityNotes, timelineNotes)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
// Check for overlapping education periods
|
||||
CheckOverlappingEducation(results);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static (bool isPlausible, string? notes) CheckDatePlausibility(DateOnly? startDate, DateOnly? endDate)
|
||||
{
|
||||
if (!startDate.HasValue || !endDate.HasValue)
|
||||
{
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
var start = startDate.Value;
|
||||
var end = endDate.Value;
|
||||
|
||||
// End date should be after start date
|
||||
if (end <= start)
|
||||
{
|
||||
return (false, "End date is before or equal to start date");
|
||||
}
|
||||
|
||||
// Check course duration is reasonable
|
||||
var years = (end.ToDateTime(TimeOnly.MinValue) - start.ToDateTime(TimeOnly.MinValue)).TotalDays / 365.25;
|
||||
|
||||
if (years < MinimumDegreeYears)
|
||||
{
|
||||
return (false, $"Course duration ({years:F1} years) is unusually short for a degree");
|
||||
}
|
||||
|
||||
if (years > MaximumDegreeYears)
|
||||
{
|
||||
return (false, $"Course duration ({years:F1} years) is unusually long");
|
||||
}
|
||||
|
||||
// Check if graduation date is in the future
|
||||
if (end > DateOnly.FromDateTime(DateTime.UtcNow))
|
||||
{
|
||||
return (true, "Graduation date is in the future - possibly currently studying");
|
||||
}
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
private static (bool isPlausible, string? notes) CheckEducationEmploymentTimeline(
|
||||
DateOnly graduationDate,
|
||||
List<EmploymentEntry> employment)
|
||||
{
|
||||
// Find the earliest employment start date
|
||||
var earliestEmployment = employment
|
||||
.Where(e => e.StartDate.HasValue)
|
||||
.OrderBy(e => e.StartDate)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (earliestEmployment?.StartDate == null)
|
||||
{
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
var employmentStart = earliestEmployment.StartDate.Value;
|
||||
|
||||
// If someone claims to have started full-time work significantly before graduating,
|
||||
// that's suspicious (unless it's clearly an internship/part-time role)
|
||||
var monthsBeforeGraduation = (graduationDate.ToDateTime(TimeOnly.MinValue) -
|
||||
employmentStart.ToDateTime(TimeOnly.MinValue)).TotalDays / 30;
|
||||
|
||||
if (monthsBeforeGraduation > 24) // More than 2 years before graduation
|
||||
{
|
||||
var isLikelyInternship = earliestEmployment.JobTitle.Contains("intern", StringComparison.OrdinalIgnoreCase) ||
|
||||
earliestEmployment.JobTitle.Contains("placement", StringComparison.OrdinalIgnoreCase) ||
|
||||
earliestEmployment.JobTitle.Contains("trainee", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!isLikelyInternship)
|
||||
{
|
||||
return (false, $"Employment at {earliestEmployment.CompanyName} started {monthsBeforeGraduation:F0} months before claimed graduation");
|
||||
}
|
||||
}
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
private static void CheckOverlappingEducation(List<EducationVerificationResult> results)
|
||||
{
|
||||
var datedResults = results
|
||||
.Where(r => r.ClaimedStartDate.HasValue && r.ClaimedEndDate.HasValue)
|
||||
.ToList();
|
||||
|
||||
for (var i = 0; i < datedResults.Count; i++)
|
||||
{
|
||||
for (var j = i + 1; j < datedResults.Count; j++)
|
||||
{
|
||||
var edu1 = datedResults[i];
|
||||
var edu2 = datedResults[j];
|
||||
|
||||
if (PeriodsOverlap(
|
||||
edu1.ClaimedStartDate!.Value, edu1.ClaimedEndDate!.Value,
|
||||
edu2.ClaimedStartDate!.Value, edu2.ClaimedEndDate!.Value))
|
||||
{
|
||||
// Find the actual index in the original results list
|
||||
var idx1 = results.IndexOf(edu1);
|
||||
var idx2 = results.IndexOf(edu2);
|
||||
|
||||
if (idx1 >= 0)
|
||||
{
|
||||
results[idx1] = edu1 with
|
||||
{
|
||||
DatePlausibilityNotes = CombineNotes(
|
||||
edu1.DatePlausibilityNotes,
|
||||
$"Overlaps with education at {edu2.ClaimedInstitution}")
|
||||
};
|
||||
}
|
||||
|
||||
if (idx2 >= 0)
|
||||
{
|
||||
results[idx2] = edu2 with
|
||||
{
|
||||
DatePlausibilityNotes = CombineNotes(
|
||||
edu2.DatePlausibilityNotes,
|
||||
$"Overlaps with education at {edu1.ClaimedInstitution}")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool PeriodsOverlap(DateOnly start1, DateOnly end1, DateOnly start2, DateOnly end2)
|
||||
{
|
||||
return start1 < end2 && start2 < end1;
|
||||
}
|
||||
|
||||
private static string? CombineNotes(string? existing, string? additional)
|
||||
{
|
||||
if (string.IsNullOrEmpty(additional))
|
||||
return existing;
|
||||
if (string.IsNullOrEmpty(existing))
|
||||
return additional;
|
||||
return $"{existing}; {additional}";
|
||||
}
|
||||
}
|
||||
133
src/RealCV.Infrastructure/Services/FileStorageService.cs
Normal file
133
src/RealCV.Infrastructure/Services/FileStorageService.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class FileStorageService : IFileStorageService
|
||||
{
|
||||
private readonly BlobContainerClient _containerClient;
|
||||
private readonly ILogger<FileStorageService> _logger;
|
||||
|
||||
public FileStorageService(
|
||||
IOptions<AzureBlobSettings> settings,
|
||||
ILogger<FileStorageService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
var blobServiceClient = new BlobServiceClient(settings.Value.ConnectionString);
|
||||
_containerClient = blobServiceClient.GetBlobContainerClient(settings.Value.ContainerName);
|
||||
}
|
||||
|
||||
public async Task<string> UploadAsync(Stream fileStream, string fileName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fileStream);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
|
||||
|
||||
var extension = Path.GetExtension(fileName);
|
||||
var uniqueBlobName = $"{Guid.NewGuid()}{extension}";
|
||||
|
||||
_logger.LogDebug("Uploading file {FileName} as blob {BlobName}", fileName, uniqueBlobName);
|
||||
|
||||
var blobClient = _containerClient.GetBlobClient(uniqueBlobName);
|
||||
|
||||
await _containerClient.CreateIfNotExistsAsync();
|
||||
|
||||
var httpHeaders = new BlobHttpHeaders
|
||||
{
|
||||
ContentType = GetContentType(extension)
|
||||
};
|
||||
|
||||
await blobClient.UploadAsync(fileStream, new BlobUploadOptions
|
||||
{
|
||||
HttpHeaders = httpHeaders,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["originalFileName"] = fileName,
|
||||
["uploadedAt"] = DateTime.UtcNow.ToString("O")
|
||||
}
|
||||
});
|
||||
|
||||
var blobUrl = blobClient.Uri.ToString();
|
||||
|
||||
_logger.LogInformation("Successfully uploaded file {FileName} to {BlobUrl}", fileName, blobUrl);
|
||||
|
||||
return blobUrl;
|
||||
}
|
||||
|
||||
public async Task<Stream> DownloadAsync(string blobUrl)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(blobUrl);
|
||||
|
||||
var blobName = ExtractBlobNameFromUrl(blobUrl);
|
||||
|
||||
_logger.LogDebug("Downloading blob {BlobName} from {BlobUrl}", blobName, blobUrl);
|
||||
|
||||
var blobClient = _containerClient.GetBlobClient(blobName);
|
||||
|
||||
// Download to memory stream to ensure proper resource management
|
||||
// The caller will own and dispose this stream
|
||||
var memoryStream = new MemoryStream();
|
||||
await blobClient.DownloadToAsync(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
_logger.LogDebug("Successfully downloaded blob {BlobName}", blobName);
|
||||
|
||||
return memoryStream;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string blobUrl)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(blobUrl);
|
||||
|
||||
var blobName = ExtractBlobNameFromUrl(blobUrl);
|
||||
|
||||
_logger.LogDebug("Deleting blob {BlobName}", blobName);
|
||||
|
||||
var blobClient = _containerClient.GetBlobClient(blobName);
|
||||
|
||||
var deleted = await blobClient.DeleteIfExistsAsync();
|
||||
|
||||
if (deleted)
|
||||
{
|
||||
_logger.LogInformation("Successfully deleted blob {BlobName}", blobName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Blob {BlobName} did not exist when attempting to delete", blobName);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractBlobNameFromUrl(string blobUrl)
|
||||
{
|
||||
if (!Uri.TryCreate(blobUrl, UriKind.Absolute, out var uri))
|
||||
{
|
||||
throw new ArgumentException($"Invalid blob URL format: '{blobUrl}'", nameof(blobUrl));
|
||||
}
|
||||
|
||||
var segments = uri.Segments;
|
||||
|
||||
// The blob name is the last segment after the container name
|
||||
// URL format: https://account.blob.core.windows.net/container/blobname
|
||||
if (segments.Length <= 2)
|
||||
{
|
||||
throw new ArgumentException($"Blob URL does not contain a valid blob name: '{blobUrl}'", nameof(blobUrl));
|
||||
}
|
||||
|
||||
return segments[^1];
|
||||
}
|
||||
|
||||
private static string GetContentType(string extension)
|
||||
{
|
||||
return extension.ToLowerInvariant() switch
|
||||
{
|
||||
".pdf" => "application/pdf",
|
||||
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".doc" => "application/msword",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
}
|
||||
}
|
||||
117
src/RealCV.Infrastructure/Services/LocalFileStorageService.cs
Normal file
117
src/RealCV.Infrastructure/Services/LocalFileStorageService.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class LocalFileStorageService : IFileStorageService
|
||||
{
|
||||
private readonly string _storagePath;
|
||||
private readonly ILogger<LocalFileStorageService> _logger;
|
||||
|
||||
public LocalFileStorageService(
|
||||
IOptions<LocalStorageSettings> settings,
|
||||
ILogger<LocalFileStorageService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_storagePath = settings.Value.StoragePath;
|
||||
|
||||
if (!Directory.Exists(_storagePath))
|
||||
{
|
||||
Directory.CreateDirectory(_storagePath);
|
||||
_logger.LogInformation("Created local storage directory: {Path}", _storagePath);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> UploadAsync(Stream fileStream, string fileName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fileStream);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
|
||||
|
||||
var extension = Path.GetExtension(fileName);
|
||||
var uniqueFileName = $"{Guid.NewGuid()}{extension}";
|
||||
var filePath = Path.Combine(_storagePath, uniqueFileName);
|
||||
|
||||
_logger.LogDebug("Uploading file {FileName} to {FilePath}", fileName, filePath);
|
||||
|
||||
await using var fileStreamOut = new FileStream(filePath, FileMode.Create, FileAccess.Write);
|
||||
await fileStream.CopyToAsync(fileStreamOut);
|
||||
|
||||
// Return a file:// URL for local storage
|
||||
var fileUrl = $"file://{filePath}";
|
||||
|
||||
_logger.LogInformation("Successfully uploaded file {FileName} to {FileUrl}", fileName, fileUrl);
|
||||
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
public async Task<Stream> DownloadAsync(string blobUrl)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(blobUrl);
|
||||
|
||||
var filePath = ExtractFilePathFromUrl(blobUrl);
|
||||
|
||||
_logger.LogDebug("Downloading file from {FilePath}", filePath);
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"File not found: {filePath}");
|
||||
}
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
await using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
_logger.LogDebug("Successfully downloaded file from {FilePath}", filePath);
|
||||
|
||||
return memoryStream;
|
||||
}
|
||||
|
||||
public Task DeleteAsync(string blobUrl)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(blobUrl);
|
||||
|
||||
var filePath = ExtractFilePathFromUrl(blobUrl);
|
||||
|
||||
_logger.LogDebug("Deleting file {FilePath}", filePath);
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
_logger.LogInformation("Successfully deleted file {FilePath}", filePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("File {FilePath} did not exist when attempting to delete", filePath);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private string ExtractFilePathFromUrl(string fileUrl)
|
||||
{
|
||||
string filePath;
|
||||
|
||||
if (fileUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
filePath = fileUrl[7..];
|
||||
}
|
||||
else
|
||||
{
|
||||
filePath = fileUrl;
|
||||
}
|
||||
|
||||
// Resolve to absolute path and validate it's within storage directory
|
||||
var fullPath = Path.GetFullPath(filePath);
|
||||
var storagePath = Path.GetFullPath(_storagePath);
|
||||
|
||||
if (!fullPath.StartsWith(storagePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Access denied: path is outside storage directory");
|
||||
}
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
205
src/RealCV.Infrastructure/Services/TimelineAnalyserService.cs
Normal file
205
src/RealCV.Infrastructure/Services/TimelineAnalyserService.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class TimelineAnalyserService : ITimelineAnalyserService
|
||||
{
|
||||
private readonly ILogger<TimelineAnalyserService> _logger;
|
||||
|
||||
private const int MinimumGapMonths = 3;
|
||||
private const int AllowedOverlapMonths = 2;
|
||||
|
||||
public TimelineAnalyserService(ILogger<TimelineAnalyserService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public TimelineAnalysisResult Analyse(List<EmploymentEntry> employmentHistory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(employmentHistory);
|
||||
|
||||
if (employmentHistory.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("No employment history to analyse");
|
||||
return new TimelineAnalysisResult
|
||||
{
|
||||
TotalGapMonths = 0,
|
||||
TotalOverlapMonths = 0,
|
||||
Gaps = [],
|
||||
Overlaps = []
|
||||
};
|
||||
}
|
||||
|
||||
// Filter entries with valid dates and sort by start date
|
||||
var sortedEmployment = employmentHistory
|
||||
.Where(e => e.StartDate.HasValue)
|
||||
.OrderBy(e => e.StartDate!.Value)
|
||||
.ToList();
|
||||
|
||||
if (sortedEmployment.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("No employment entries with valid dates to analyse");
|
||||
return new TimelineAnalysisResult
|
||||
{
|
||||
TotalGapMonths = 0,
|
||||
TotalOverlapMonths = 0,
|
||||
Gaps = [],
|
||||
Overlaps = []
|
||||
};
|
||||
}
|
||||
|
||||
var gaps = DetectGaps(sortedEmployment);
|
||||
var overlaps = DetectOverlaps(sortedEmployment);
|
||||
|
||||
var totalGapMonths = gaps.Sum(g => g.Months);
|
||||
var totalOverlapMonths = overlaps.Sum(o => o.Months);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Timeline analysis complete: {GapCount} gaps ({TotalGapMonths} months), {OverlapCount} overlaps ({TotalOverlapMonths} months)",
|
||||
gaps.Count, totalGapMonths, overlaps.Count, totalOverlapMonths);
|
||||
|
||||
return new TimelineAnalysisResult
|
||||
{
|
||||
TotalGapMonths = totalGapMonths,
|
||||
TotalOverlapMonths = totalOverlapMonths,
|
||||
Gaps = gaps,
|
||||
Overlaps = overlaps
|
||||
};
|
||||
}
|
||||
|
||||
private List<TimelineGap> DetectGaps(List<EmploymentEntry> sortedEmployment)
|
||||
{
|
||||
var gaps = new List<TimelineGap>();
|
||||
|
||||
for (var i = 0; i < sortedEmployment.Count - 1; i++)
|
||||
{
|
||||
var current = sortedEmployment[i];
|
||||
var next = sortedEmployment[i + 1];
|
||||
|
||||
// Get the effective end date for the current position
|
||||
var currentEndDate = GetEffectiveEndDate(current);
|
||||
var nextStartDate = next.StartDate!.Value;
|
||||
|
||||
// Skip if there's no gap or overlap
|
||||
if (currentEndDate >= nextStartDate)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var gapMonths = CalculateMonthsDifference(currentEndDate, nextStartDate);
|
||||
|
||||
// Only report gaps of 3+ months
|
||||
if (gapMonths >= MinimumGapMonths)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Detected {Months} month gap between {EndDate} and {StartDate}",
|
||||
gapMonths, currentEndDate, nextStartDate);
|
||||
|
||||
gaps.Add(new TimelineGap
|
||||
{
|
||||
StartDate = currentEndDate,
|
||||
EndDate = nextStartDate,
|
||||
Months = gapMonths
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return gaps;
|
||||
}
|
||||
|
||||
private List<TimelineOverlap> DetectOverlaps(List<EmploymentEntry> sortedEmployment)
|
||||
{
|
||||
var overlaps = new List<TimelineOverlap>();
|
||||
|
||||
for (var i = 0; i < sortedEmployment.Count; i++)
|
||||
{
|
||||
for (var j = i + 1; j < sortedEmployment.Count; j++)
|
||||
{
|
||||
var earlier = sortedEmployment[i];
|
||||
var later = sortedEmployment[j];
|
||||
|
||||
var overlap = CalculateOverlap(earlier, later);
|
||||
|
||||
if (overlap is not null && overlap.Value.Months > AllowedOverlapMonths)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Detected {Months} month overlap between {Company1} and {Company2}",
|
||||
overlap.Value.Months, earlier.CompanyName, later.CompanyName);
|
||||
|
||||
overlaps.Add(new TimelineOverlap
|
||||
{
|
||||
Company1 = earlier.CompanyName,
|
||||
Company2 = later.CompanyName,
|
||||
OverlapStart = overlap.Value.Start,
|
||||
OverlapEnd = overlap.Value.End,
|
||||
Months = overlap.Value.Months
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return overlaps;
|
||||
}
|
||||
|
||||
private static (DateOnly Start, DateOnly End, int Months)? CalculateOverlap(
|
||||
EmploymentEntry earlier,
|
||||
EmploymentEntry later)
|
||||
{
|
||||
if (!earlier.StartDate.HasValue || !later.StartDate.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var earlierEnd = GetEffectiveEndDate(earlier);
|
||||
var laterStart = later.StartDate.Value;
|
||||
|
||||
// No overlap if earlier job ended before later job started
|
||||
if (earlierEnd <= laterStart)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var laterEnd = GetEffectiveEndDate(later);
|
||||
|
||||
// The overlap period
|
||||
var overlapStart = laterStart;
|
||||
var overlapEnd = earlierEnd < laterEnd ? earlierEnd : laterEnd;
|
||||
|
||||
if (overlapStart >= overlapEnd)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var months = CalculateMonthsDifference(overlapStart, overlapEnd);
|
||||
|
||||
return (overlapStart, overlapEnd, months);
|
||||
}
|
||||
|
||||
private static DateOnly GetEffectiveEndDate(EmploymentEntry entry)
|
||||
{
|
||||
if (entry.EndDate.HasValue)
|
||||
{
|
||||
return entry.EndDate.Value;
|
||||
}
|
||||
|
||||
// If marked as current or no end date, use today
|
||||
return DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
private static int CalculateMonthsDifference(DateOnly startDate, DateOnly endDate)
|
||||
{
|
||||
var yearDiff = endDate.Year - startDate.Year;
|
||||
var monthDiff = endDate.Month - startDate.Month;
|
||||
var totalMonths = (yearDiff * 12) + monthDiff;
|
||||
|
||||
// Add a month if we've passed the day in the month
|
||||
if (endDate.Day >= startDate.Day)
|
||||
{
|
||||
totalMonths++;
|
||||
}
|
||||
|
||||
return Math.Max(0, totalMonths);
|
||||
}
|
||||
}
|
||||
28
src/RealCV.Infrastructure/Services/UserContextService.cs
Normal file
28
src/RealCV.Infrastructure/Services/UserContextService.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using RealCV.Application.Interfaces;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class UserContextService : IUserContextService
|
||||
{
|
||||
private readonly AuthenticationStateProvider _authenticationStateProvider;
|
||||
|
||||
public UserContextService(AuthenticationStateProvider authenticationStateProvider)
|
||||
{
|
||||
_authenticationStateProvider = authenticationStateProvider;
|
||||
}
|
||||
|
||||
public async Task<Guid?> GetCurrentUserIdAsync()
|
||||
{
|
||||
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
|
||||
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user