Add audit logging, processing stages, delete functionality, and bug fixes
- Add audit logging system for tracking CV uploads, processing, deletion, report views, and PDF exports for billing/reference purposes - Add processing stage display on dashboard instead of generic "Processing" - Add delete button for CV checks on dashboard - Fix duplicate primary key error in CompanyCache (race condition) - Fix DbContext concurrency in Dashboard (concurrent delete/load operations) - Fix ProcessCVCheckJob to handle deleted records gracefully - Fix duplicate flags in verification report by deduplicating on Title+Description - Remove internal cache notes from verification results - Add EF migrations for ProcessingStage and AuditLog table Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ public sealed record CVCheckDto
|
|||||||
public required string OriginalFileName { get; init; }
|
public required string OriginalFileName { get; init; }
|
||||||
public required string Status { get; init; }
|
public required string Status { get; init; }
|
||||||
public int? VeracityScore { get; init; }
|
public int? VeracityScore { get; init; }
|
||||||
|
public string? ProcessingStage { get; init; }
|
||||||
public required DateTime CreatedAt { get; init; }
|
public required DateTime CreatedAt { get; init; }
|
||||||
public DateTime? CompletedAt { get; init; }
|
public DateTime? CompletedAt { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/TrueCV.Application/Interfaces/IAuditService.cs
Normal file
18
src/TrueCV.Application/Interfaces/IAuditService.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace TrueCV.Application.Interfaces;
|
||||||
|
|
||||||
|
public interface IAuditService
|
||||||
|
{
|
||||||
|
Task LogAsync(Guid userId, string action, string? entityType = null, Guid? entityId = null, string? details = null, string? ipAddress = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AuditActions
|
||||||
|
{
|
||||||
|
public const string CVUploaded = "CV_UPLOADED";
|
||||||
|
public const string CVProcessed = "CV_PROCESSED";
|
||||||
|
public const string CVDeleted = "CV_DELETED";
|
||||||
|
public const string ReportViewed = "REPORT_VIEWED";
|
||||||
|
public const string ReportExported = "REPORT_EXPORTED";
|
||||||
|
public const string UserLogin = "USER_LOGIN";
|
||||||
|
public const string UserLogout = "USER_LOGOUT";
|
||||||
|
public const string UserRegistered = "USER_REGISTERED";
|
||||||
|
}
|
||||||
@@ -10,4 +10,5 @@ public interface ICVCheckService
|
|||||||
Task<CVCheckDto?> GetCheckForUserAsync(Guid id, Guid userId);
|
Task<CVCheckDto?> GetCheckForUserAsync(Guid id, Guid userId);
|
||||||
Task<List<CVCheckDto>> GetUserChecksAsync(Guid userId);
|
Task<List<CVCheckDto>> GetUserChecksAsync(Guid userId);
|
||||||
Task<VeracityReport?> GetReportAsync(Guid checkId, Guid userId);
|
Task<VeracityReport?> GetReportAsync(Guid checkId, Guid userId);
|
||||||
|
Task<bool> DeleteCheckAsync(Guid checkId, Guid userId);
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/TrueCV.Domain/Entities/AuditLog.cs
Normal file
28
src/TrueCV.Domain/Entities/AuditLog.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace TrueCV.Domain.Entities;
|
||||||
|
|
||||||
|
public class AuditLog
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(64)]
|
||||||
|
public string Action { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[MaxLength(128)]
|
||||||
|
public string? EntityType { get; set; }
|
||||||
|
|
||||||
|
public Guid? EntityId { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(1024)]
|
||||||
|
public string? Details { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(64)]
|
||||||
|
public string? IpAddress { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -24,6 +24,9 @@ public class CVCheck
|
|||||||
|
|
||||||
public int? VeracityScore { get; set; }
|
public int? VeracityScore { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(64)]
|
||||||
|
public string? ProcessingStage { get; set; }
|
||||||
|
|
||||||
public string? ReportJson { get; set; }
|
public string? ReportJson { get; set; }
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
|
|||||||
public DbSet<CVCheck> CVChecks => Set<CVCheck>();
|
public DbSet<CVCheck> CVChecks => Set<CVCheck>();
|
||||||
public DbSet<CVFlag> CVFlags => Set<CVFlag>();
|
public DbSet<CVFlag> CVFlags => Set<CVFlag>();
|
||||||
public DbSet<CompanyCache> CompanyCache => Set<CompanyCache>();
|
public DbSet<CompanyCache> CompanyCache => Set<CompanyCache>();
|
||||||
|
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
@@ -25,6 +26,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
|
|||||||
ConfigureCVCheck(builder);
|
ConfigureCVCheck(builder);
|
||||||
ConfigureCVFlag(builder);
|
ConfigureCVFlag(builder);
|
||||||
ConfigureCompanyCache(builder);
|
ConfigureCompanyCache(builder);
|
||||||
|
ConfigureAuditLog(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureApplicationUser(ModelBuilder builder)
|
private static void ConfigureApplicationUser(ModelBuilder builder)
|
||||||
@@ -94,6 +96,21 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var newCVChecks = ChangeTracker.Entries<CVCheck>()
|
var newCVChecks = ChangeTracker.Entries<CVCheck>()
|
||||||
|
|||||||
456
src/TrueCV.Infrastructure/Data/Migrations/20260120191035_AddProcessingStageToCV.Designer.cs
generated
Normal file
456
src/TrueCV.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 TrueCV.Infrastructure.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrueCV.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("TrueCV.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("TrueCV.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("TrueCV.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("TrueCV.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("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrueCV.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("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany("CVChecks")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrueCV.Domain.Entities.CVCheck", "CVCheck")
|
||||||
|
.WithMany("Flags")
|
||||||
|
.HasForeignKey("CVCheckId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("CVCheck");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Flags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrueCV.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 TrueCV.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/TrueCV.Infrastructure/Data/Migrations/20260120194532_AddAuditLogTable.Designer.cs
generated
Normal file
502
src/TrueCV.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 TrueCV.Infrastructure.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrueCV.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("TrueCV.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("TrueCV.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("TrueCV.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("TrueCV.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("TrueCV.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("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrueCV.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("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany("CVChecks")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrueCV.Domain.Entities.CVCheck", "CVCheck")
|
||||||
|
.WithMany("Flags")
|
||||||
|
.HasForeignKey("CVCheckId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("CVCheck");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Flags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrueCV.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 TrueCV.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -153,6 +153,52 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
|||||||
b.ToTable("AspNetUserTokens", (string)null);
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrueCV.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("TrueCV.Domain.Entities.CVCheck", b =>
|
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -178,6 +224,10 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
|||||||
.HasMaxLength(512)
|
.HasMaxLength(512)
|
||||||
.HasColumnType("nvarchar(512)");
|
.HasColumnType("nvarchar(512)");
|
||||||
|
|
||||||
|
b.Property<string>("ProcessingStage")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
b.Property<string>("ReportJson")
|
b.Property<string>("ReportJson")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
@@ -189,9 +239,6 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
|||||||
b.Property<Guid>("UserId")
|
b.Property<Guid>("UserId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
b.Property<Guid?>("UserId1")
|
|
||||||
.HasColumnType("uniqueidentifier");
|
|
||||||
|
|
||||||
b.Property<int?>("VeracityScore")
|
b.Property<int?>("VeracityScore")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
@@ -203,8 +250,6 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
|||||||
b.HasIndex("UserId")
|
b.HasIndex("UserId")
|
||||||
.HasDatabaseName("IX_CVChecks_UserId");
|
.HasDatabaseName("IX_CVChecks_UserId");
|
||||||
|
|
||||||
b.HasIndex("UserId1");
|
|
||||||
|
|
||||||
b.ToTable("CVChecks");
|
b.ToTable("CVChecks");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -254,6 +299,10 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
|||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("nvarchar(32)");
|
.HasColumnType("nvarchar(32)");
|
||||||
|
|
||||||
|
b.Property<string>("AccountsCategory")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
b.Property<DateTime>("CachedAt")
|
b.Property<DateTime>("CachedAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
@@ -262,12 +311,20 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
|||||||
.HasMaxLength(512)
|
.HasMaxLength(512)
|
||||||
.HasColumnType("nvarchar(512)");
|
.HasColumnType("nvarchar(512)");
|
||||||
|
|
||||||
|
b.Property<string>("CompanyType")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
b.Property<DateOnly?>("DissolutionDate")
|
b.Property<DateOnly?>("DissolutionDate")
|
||||||
.HasColumnType("date");
|
.HasColumnType("date");
|
||||||
|
|
||||||
b.Property<DateOnly?>("IncorporationDate")
|
b.Property<DateOnly?>("IncorporationDate")
|
||||||
.HasColumnType("date");
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<string>("SicCodesJson")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
b.Property<string>("Status")
|
b.Property<string>("Status")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
@@ -278,32 +335,6 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
|||||||
b.ToTable("CompanyCache");
|
b.ToTable("CompanyCache");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TrueCV.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("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
modelBuilder.Entity("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -440,10 +471,6 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
|||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("TrueCV.Domain.Entities.User", null)
|
|
||||||
.WithMany("CVChecks")
|
|
||||||
.HasForeignKey("UserId1");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
||||||
@@ -462,11 +489,6 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
|||||||
b.Navigation("Flags");
|
b.Navigation("Flags");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TrueCV.Domain.Entities.User", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("CVChecks");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
modelBuilder.Entity("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("CVChecks");
|
b.Navigation("CVChecks");
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<ITimelineAnalyserService, TimelineAnalyserService>();
|
services.AddScoped<ITimelineAnalyserService, TimelineAnalyserService>();
|
||||||
services.AddScoped<ICVCheckService, CVCheckService>();
|
services.AddScoped<ICVCheckService, CVCheckService>();
|
||||||
services.AddScoped<IUserContextService, UserContextService>();
|
services.AddScoped<IUserContextService, UserContextService>();
|
||||||
|
services.AddScoped<IAuditService, AuditService>();
|
||||||
|
|
||||||
// Register file storage - use local storage if configured, otherwise Azure
|
// Register file storage - use local storage if configured, otherwise Azure
|
||||||
var useLocalStorage = configuration.GetValue<bool>("UseLocalStorage");
|
var useLocalStorage = configuration.GetValue<bool>("UseLocalStorage");
|
||||||
|
|||||||
@@ -200,10 +200,16 @@ public sealed record CompaniesHouseCompany
|
|||||||
|
|
||||||
public sealed record CompaniesHouseAccounts
|
public sealed record CompaniesHouseAccounts
|
||||||
{
|
{
|
||||||
public string? AccountingReferenceDate { get; init; }
|
public CompaniesHouseAccountingReferenceDate? AccountingReferenceDate { get; init; }
|
||||||
public CompaniesHouseLastAccounts? LastAccounts { 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 sealed record CompaniesHouseLastAccounts
|
||||||
{
|
{
|
||||||
public string? MadeUpTo { get; init; }
|
public string? MadeUpTo { get; init; }
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public sealed class ProcessCVCheckJob
|
|||||||
private readonly ICompanyVerifierService _companyVerifierService;
|
private readonly ICompanyVerifierService _companyVerifierService;
|
||||||
private readonly IEducationVerifierService _educationVerifierService;
|
private readonly IEducationVerifierService _educationVerifierService;
|
||||||
private readonly ITimelineAnalyserService _timelineAnalyserService;
|
private readonly ITimelineAnalyserService _timelineAnalyserService;
|
||||||
|
private readonly IAuditService _auditService;
|
||||||
private readonly ILogger<ProcessCVCheckJob> _logger;
|
private readonly ILogger<ProcessCVCheckJob> _logger;
|
||||||
|
|
||||||
private const int BaseScore = 100;
|
private const int BaseScore = 100;
|
||||||
@@ -41,6 +42,7 @@ public sealed class ProcessCVCheckJob
|
|||||||
ICompanyVerifierService companyVerifierService,
|
ICompanyVerifierService companyVerifierService,
|
||||||
IEducationVerifierService educationVerifierService,
|
IEducationVerifierService educationVerifierService,
|
||||||
ITimelineAnalyserService timelineAnalyserService,
|
ITimelineAnalyserService timelineAnalyserService,
|
||||||
|
IAuditService auditService,
|
||||||
ILogger<ProcessCVCheckJob> logger)
|
ILogger<ProcessCVCheckJob> logger)
|
||||||
{
|
{
|
||||||
_dbContext = dbContext;
|
_dbContext = dbContext;
|
||||||
@@ -49,6 +51,7 @@ public sealed class ProcessCVCheckJob
|
|||||||
_companyVerifierService = companyVerifierService;
|
_companyVerifierService = companyVerifierService;
|
||||||
_educationVerifierService = educationVerifierService;
|
_educationVerifierService = educationVerifierService;
|
||||||
_timelineAnalyserService = timelineAnalyserService;
|
_timelineAnalyserService = timelineAnalyserService;
|
||||||
|
_auditService = auditService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +72,7 @@ public sealed class ProcessCVCheckJob
|
|||||||
{
|
{
|
||||||
// Step 1: Update status to Processing
|
// Step 1: Update status to Processing
|
||||||
cvCheck.Status = CheckStatus.Processing;
|
cvCheck.Status = CheckStatus.Processing;
|
||||||
|
cvCheck.ProcessingStage = "Downloading CV";
|
||||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
_logger.LogDebug("CV check {CheckId} status updated to Processing", cvCheckId);
|
_logger.LogDebug("CV check {CheckId} status updated to Processing", cvCheckId);
|
||||||
@@ -79,6 +83,9 @@ public sealed class ProcessCVCheckJob
|
|||||||
_logger.LogDebug("Downloaded CV file for check {CheckId}", cvCheckId);
|
_logger.LogDebug("Downloaded CV file for check {CheckId}", cvCheckId);
|
||||||
|
|
||||||
// Step 3: Parse CV
|
// Step 3: Parse CV
|
||||||
|
cvCheck.ProcessingStage = "Parsing CV";
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
var cvData = await _cvParserService.ParseAsync(fileStream, cvCheck.OriginalFileName, cancellationToken);
|
var cvData = await _cvParserService.ParseAsync(fileStream, cvCheck.OriginalFileName, cancellationToken);
|
||||||
|
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
@@ -87,6 +94,7 @@ public sealed class ProcessCVCheckJob
|
|||||||
|
|
||||||
// Step 4: Save extracted data
|
// Step 4: Save extracted data
|
||||||
cvCheck.ExtractedDataJson = JsonSerializer.Serialize(cvData, JsonDefaults.CamelCaseIndented);
|
cvCheck.ExtractedDataJson = JsonSerializer.Serialize(cvData, JsonDefaults.CamelCaseIndented);
|
||||||
|
cvCheck.ProcessingStage = "Verifying Employment";
|
||||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// Step 5: Verify each employment entry (parallelized with rate limiting)
|
// Step 5: Verify each employment entry (parallelized with rate limiting)
|
||||||
@@ -128,9 +136,14 @@ public sealed class ProcessCVCheckJob
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 5b: Verify director claims against Companies House officers
|
// Step 5b: Verify director claims against Companies House officers
|
||||||
|
cvCheck.ProcessingStage = "Verifying Directors";
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
await VerifyDirectorClaims(cvData.FullName, verificationResults, cancellationToken);
|
await VerifyDirectorClaims(cvData.FullName, verificationResults, cancellationToken);
|
||||||
|
|
||||||
// Step 6: Verify education entries
|
// Step 6: Verify education entries
|
||||||
|
cvCheck.ProcessingStage = "Verifying Education";
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
var educationResults = _educationVerifierService.VerifyAll(
|
var educationResults = _educationVerifierService.VerifyAll(
|
||||||
cvData.Education,
|
cvData.Education,
|
||||||
cvData.Employment);
|
cvData.Employment);
|
||||||
@@ -143,6 +156,9 @@ public sealed class ProcessCVCheckJob
|
|||||||
educationResults.Count(e => e.IsDiplomaMill));
|
educationResults.Count(e => e.IsDiplomaMill));
|
||||||
|
|
||||||
// Step 7: Analyse timeline
|
// Step 7: Analyse timeline
|
||||||
|
cvCheck.ProcessingStage = "Analyzing Timeline";
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
var timelineAnalysis = _timelineAnalyserService.Analyse(cvData.Employment);
|
var timelineAnalysis = _timelineAnalyserService.Analyse(cvData.Employment);
|
||||||
|
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
@@ -150,6 +166,8 @@ public sealed class ProcessCVCheckJob
|
|||||||
cvCheckId, timelineAnalysis.Gaps.Count, timelineAnalysis.Overlaps.Count);
|
cvCheckId, timelineAnalysis.Gaps.Count, timelineAnalysis.Overlaps.Count);
|
||||||
|
|
||||||
// Step 8: Calculate veracity score
|
// Step 8: Calculate veracity score
|
||||||
|
cvCheck.ProcessingStage = "Calculating Score";
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
var (score, flags) = CalculateVeracityScore(verificationResults, educationResults, timelineAnalysis, cvData);
|
var (score, flags) = CalculateVeracityScore(verificationResults, educationResults, timelineAnalysis, cvData);
|
||||||
|
|
||||||
_logger.LogDebug("Calculated veracity score for check {CheckId}: {Score}", cvCheckId, score);
|
_logger.LogDebug("Calculated veracity score for check {CheckId}: {Score}", cvCheckId, score);
|
||||||
@@ -184,6 +202,9 @@ public sealed class ProcessCVCheckJob
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 10: Generate veracity report
|
// Step 10: Generate veracity report
|
||||||
|
cvCheck.ProcessingStage = "Generating Report";
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
var report = new VeracityReport
|
var report = new VeracityReport
|
||||||
{
|
{
|
||||||
OverallScore = score,
|
OverallScore = score,
|
||||||
@@ -200,20 +221,32 @@ public sealed class ProcessCVCheckJob
|
|||||||
|
|
||||||
// Step 11: Update status to Completed
|
// Step 11: Update status to Completed
|
||||||
cvCheck.Status = CheckStatus.Completed;
|
cvCheck.Status = CheckStatus.Completed;
|
||||||
|
cvCheck.ProcessingStage = null; // Clear stage on completion
|
||||||
cvCheck.CompletedAt = DateTime.UtcNow;
|
cvCheck.CompletedAt = DateTime.UtcNow;
|
||||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"CV check {CheckId} completed successfully with score {Score}",
|
"CV check {CheckId} completed successfully with score {Score}",
|
||||||
cvCheckId, score);
|
cvCheckId, score);
|
||||||
|
|
||||||
|
await _auditService.LogAsync(cvCheck.UserId, AuditActions.CVProcessed, "CVCheck", cvCheckId, $"Score: {score}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error processing CV check {CheckId}", cvCheckId);
|
_logger.LogError(ex, "Error processing CV check {CheckId}", cvCheckId);
|
||||||
|
|
||||||
cvCheck.Status = CheckStatus.Failed;
|
try
|
||||||
// Use CancellationToken.None to ensure failure status is saved even if original token is cancelled
|
{
|
||||||
await _dbContext.SaveChangesAsync(CancellationToken.None);
|
cvCheck.Status = CheckStatus.Failed;
|
||||||
|
// Use CancellationToken.None to ensure failure status is saved even if original token is cancelled
|
||||||
|
await _dbContext.SaveChangesAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException)
|
||||||
|
{
|
||||||
|
// Record was deleted during processing - nothing to update
|
||||||
|
_logger.LogWarning("CV check {CheckId} was deleted during processing", cvCheckId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@@ -382,10 +415,19 @@ public sealed class ProcessCVCheckJob
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure score doesn't go below 0
|
// Deduplicate flags based on Title + Description
|
||||||
score = Math.Max(0, score);
|
var uniqueFlags = flags
|
||||||
|
.GroupBy(f => (f.Title, f.Description))
|
||||||
|
.Select(g => g.First())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return (score, flags);
|
// Recalculate score based on unique flags
|
||||||
|
var uniqueScore = BaseScore + uniqueFlags.Sum(f => f.ScoreImpact);
|
||||||
|
|
||||||
|
// Ensure score doesn't go below 0
|
||||||
|
uniqueScore = Math.Max(0, uniqueScore);
|
||||||
|
|
||||||
|
return (uniqueScore, uniqueFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetScoreLabel(int score)
|
private static string GetScoreLabel(int score)
|
||||||
@@ -420,8 +462,13 @@ public sealed class ProcessCVCheckJob
|
|||||||
List<CompanyVerificationResult> verificationResults,
|
List<CompanyVerificationResult> verificationResults,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Find all director claims at verified companies
|
// Find all director claims at verified companies - use ToList() to avoid modifying during enumeration
|
||||||
foreach (var result in verificationResults.Where(v => v.IsVerified && !string.IsNullOrEmpty(v.MatchedCompanyNumber)))
|
var directorCandidates = verificationResults
|
||||||
|
.Select((result, index) => (result, index))
|
||||||
|
.Where(x => x.result.IsVerified && !string.IsNullOrEmpty(x.result.MatchedCompanyNumber))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var (result, index) in directorCandidates)
|
||||||
{
|
{
|
||||||
var jobTitle = result.ClaimedJobTitle?.ToLowerInvariant() ?? "";
|
var jobTitle = result.ClaimedJobTitle?.ToLowerInvariant() ?? "";
|
||||||
|
|
||||||
@@ -446,7 +493,7 @@ public sealed class ProcessCVCheckJob
|
|||||||
if (isVerifiedDirector == false)
|
if (isVerifiedDirector == false)
|
||||||
{
|
{
|
||||||
// Add a flag for unverified director claim
|
// Add a flag for unverified director claim
|
||||||
var flags = result.Flags.ToList();
|
var flags = (result.Flags ?? []).ToList();
|
||||||
flags.Add(new CompanyVerificationFlag
|
flags.Add(new CompanyVerificationFlag
|
||||||
{
|
{
|
||||||
Type = "UnverifiedDirectorClaim",
|
Type = "UnverifiedDirectorClaim",
|
||||||
@@ -456,7 +503,6 @@ public sealed class ProcessCVCheckJob
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update the result with the new flag
|
// Update the result with the new flag
|
||||||
var index = verificationResults.IndexOf(result);
|
|
||||||
verificationResults[index] = result with { Flags = flags };
|
verificationResults[index] = result with { Flags = flags };
|
||||||
|
|
||||||
_logger.LogWarning(
|
_logger.LogWarning(
|
||||||
|
|||||||
52
src/TrueCV.Infrastructure/Services/AuditService.cs
Normal file
52
src/TrueCV.Infrastructure/Services/AuditService.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TrueCV.Application.Interfaces;
|
||||||
|
using TrueCV.Domain.Entities;
|
||||||
|
using TrueCV.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace TrueCV.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,17 +18,20 @@ public sealed class CVCheckService : ICVCheckService
|
|||||||
private readonly ApplicationDbContext _dbContext;
|
private readonly ApplicationDbContext _dbContext;
|
||||||
private readonly IFileStorageService _fileStorageService;
|
private readonly IFileStorageService _fileStorageService;
|
||||||
private readonly IBackgroundJobClient _backgroundJobClient;
|
private readonly IBackgroundJobClient _backgroundJobClient;
|
||||||
|
private readonly IAuditService _auditService;
|
||||||
private readonly ILogger<CVCheckService> _logger;
|
private readonly ILogger<CVCheckService> _logger;
|
||||||
|
|
||||||
public CVCheckService(
|
public CVCheckService(
|
||||||
ApplicationDbContext dbContext,
|
ApplicationDbContext dbContext,
|
||||||
IFileStorageService fileStorageService,
|
IFileStorageService fileStorageService,
|
||||||
IBackgroundJobClient backgroundJobClient,
|
IBackgroundJobClient backgroundJobClient,
|
||||||
|
IAuditService auditService,
|
||||||
ILogger<CVCheckService> logger)
|
ILogger<CVCheckService> logger)
|
||||||
{
|
{
|
||||||
_dbContext = dbContext;
|
_dbContext = dbContext;
|
||||||
_fileStorageService = fileStorageService;
|
_fileStorageService = fileStorageService;
|
||||||
_backgroundJobClient = backgroundJobClient;
|
_backgroundJobClient = backgroundJobClient;
|
||||||
|
_auditService = auditService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +69,8 @@ public sealed class CVCheckService : ICVCheckService
|
|||||||
"CV check {CheckId} created for user {UserId}, processing queued",
|
"CV check {CheckId} created for user {UserId}, processing queued",
|
||||||
cvCheck.Id, userId);
|
cvCheck.Id, userId);
|
||||||
|
|
||||||
|
await _auditService.LogAsync(userId, AuditActions.CVUploaded, "CVCheck", cvCheck.Id, $"File: {fileName}");
|
||||||
|
|
||||||
return cvCheck.Id;
|
return cvCheck.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +155,33 @@ public sealed class CVCheckService : ICVCheckService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
private static CVCheckDto MapToDto(CVCheck cvCheck)
|
||||||
{
|
{
|
||||||
return new CVCheckDto
|
return new CVCheckDto
|
||||||
@@ -158,6 +190,7 @@ public sealed class CVCheckService : ICVCheckService
|
|||||||
OriginalFileName = cvCheck.OriginalFileName,
|
OriginalFileName = cvCheck.OriginalFileName,
|
||||||
Status = cvCheck.Status.ToString(),
|
Status = cvCheck.Status.ToString(),
|
||||||
VeracityScore = cvCheck.VeracityScore,
|
VeracityScore = cvCheck.VeracityScore,
|
||||||
|
ProcessingStage = cvCheck.ProcessingStage,
|
||||||
CreatedAt = cvCheck.CreatedAt,
|
CreatedAt = cvCheck.CreatedAt,
|
||||||
CompletedAt = cvCheck.CompletedAt
|
CompletedAt = cvCheck.CompletedAt
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
|||||||
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||||
private readonly ILogger<CompanyVerifierService> _logger;
|
private readonly ILogger<CompanyVerifierService> _logger;
|
||||||
|
|
||||||
private const int FuzzyMatchThreshold = 70;
|
private const int FuzzyMatchThreshold = 85;
|
||||||
private const int CacheExpirationDays = 30;
|
private const int CacheExpirationDays = 30;
|
||||||
|
|
||||||
// SIC codes for tech/software companies
|
// SIC codes for tech/software companies
|
||||||
@@ -135,7 +135,7 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
|||||||
MatchedCompanyNumber = match.Item.CompanyNumber,
|
MatchedCompanyNumber = match.Item.CompanyNumber,
|
||||||
MatchScore = match.Score,
|
MatchScore = match.Score,
|
||||||
IsVerified = true,
|
IsVerified = true,
|
||||||
VerificationNotes = $"Matched with {match.Score}% confidence",
|
VerificationNotes = null,
|
||||||
ClaimedStartDate = startDate,
|
ClaimedStartDate = startDate,
|
||||||
ClaimedEndDate = endDate,
|
ClaimedEndDate = endDate,
|
||||||
CompanyType = companyType,
|
CompanyType = companyType,
|
||||||
@@ -549,7 +549,8 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var matches = cachedCompanies
|
var matches = cachedCompanies
|
||||||
.Select(c => new { Company = c, Score = Fuzz.Ratio(companyName.ToUpperInvariant(), c.CompanyName.ToUpperInvariant()) })
|
.Where(c => !string.IsNullOrWhiteSpace(c.CompanyName))
|
||||||
|
.Select(c => new { Company = c, Score = Fuzz.TokenSetRatio(companyName.ToUpperInvariant(), c.CompanyName.ToUpperInvariant()) })
|
||||||
.Where(m => m.Score >= FuzzyMatchThreshold)
|
.Where(m => m.Score >= FuzzyMatchThreshold)
|
||||||
.OrderByDescending(m => m.Score)
|
.OrderByDescending(m => m.Score)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
@@ -564,7 +565,8 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
|||||||
var normalizedSearch = companyName.ToUpperInvariant();
|
var normalizedSearch = companyName.ToUpperInvariant();
|
||||||
|
|
||||||
var matches = items
|
var matches = items
|
||||||
.Select(item => (Item: item, Score: Fuzz.Ratio(normalizedSearch, item.Title.ToUpperInvariant())))
|
.Where(item => !string.IsNullOrWhiteSpace(item.Title))
|
||||||
|
.Select(item => (Item: item, Score: Fuzz.TokenSetRatio(normalizedSearch, item.Title.ToUpperInvariant())))
|
||||||
.Where(m => m.Score >= FuzzyMatchThreshold)
|
.Where(m => m.Score >= FuzzyMatchThreshold)
|
||||||
.OrderByDescending(m => m.Score)
|
.OrderByDescending(m => m.Score)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -574,45 +576,53 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
|||||||
|
|
||||||
private async Task CacheCompanyAsync(CompaniesHouseSearchItem item, CompaniesHouseCompany? details)
|
private async Task CacheCompanyAsync(CompaniesHouseSearchItem item, CompaniesHouseCompany? details)
|
||||||
{
|
{
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
try
|
||||||
|
|
||||||
var existingCache = await dbContext.CompanyCache
|
|
||||||
.FirstOrDefaultAsync(c => c.CompanyNumber == item.CompanyNumber);
|
|
||||||
|
|
||||||
var sicCodes = details?.SicCodes ?? item.SicCodes;
|
|
||||||
var sicCodesJson = sicCodes != null ? JsonSerializer.Serialize(sicCodes) : null;
|
|
||||||
var accountsCategory = details?.Accounts?.LastAccounts?.Type;
|
|
||||||
|
|
||||||
if (existingCache is not null)
|
|
||||||
{
|
{
|
||||||
existingCache.CompanyName = item.Title;
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
existingCache.Status = item.CompanyStatus ?? "Unknown";
|
|
||||||
existingCache.CompanyType = item.CompanyType;
|
var existingCache = await dbContext.CompanyCache
|
||||||
existingCache.IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation);
|
.FirstOrDefaultAsync(c => c.CompanyNumber == item.CompanyNumber);
|
||||||
existingCache.DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation);
|
|
||||||
existingCache.AccountsCategory = accountsCategory;
|
var sicCodes = details?.SicCodes ?? item.SicCodes;
|
||||||
existingCache.SicCodesJson = sicCodesJson;
|
var sicCodesJson = sicCodes != null ? JsonSerializer.Serialize(sicCodes) : null;
|
||||||
existingCache.CachedAt = DateTime.UtcNow;
|
var accountsCategory = details?.Accounts?.LastAccounts?.Type;
|
||||||
}
|
|
||||||
else
|
if (existingCache is not null)
|
||||||
{
|
|
||||||
var cacheEntry = new CompanyCache
|
|
||||||
{
|
{
|
||||||
CompanyNumber = item.CompanyNumber,
|
existingCache.CompanyName = item.Title;
|
||||||
CompanyName = item.Title,
|
existingCache.Status = item.CompanyStatus ?? "Unknown";
|
||||||
Status = item.CompanyStatus ?? "Unknown",
|
existingCache.CompanyType = item.CompanyType;
|
||||||
CompanyType = item.CompanyType,
|
existingCache.IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation);
|
||||||
IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation),
|
existingCache.DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation);
|
||||||
DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation),
|
existingCache.AccountsCategory = accountsCategory;
|
||||||
AccountsCategory = accountsCategory,
|
existingCache.SicCodesJson = sicCodesJson;
|
||||||
SicCodesJson = sicCodesJson,
|
existingCache.CachedAt = DateTime.UtcNow;
|
||||||
CachedAt = DateTime.UtcNow
|
}
|
||||||
};
|
else
|
||||||
|
{
|
||||||
|
var cacheEntry = new CompanyCache
|
||||||
|
{
|
||||||
|
CompanyNumber = item.CompanyNumber,
|
||||||
|
CompanyName = item.Title,
|
||||||
|
Status = item.CompanyStatus ?? "Unknown",
|
||||||
|
CompanyType = item.CompanyType,
|
||||||
|
IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation),
|
||||||
|
DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation),
|
||||||
|
AccountsCategory = accountsCategory,
|
||||||
|
SicCodesJson = sicCodesJson,
|
||||||
|
CachedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
dbContext.CompanyCache.Add(cacheEntry);
|
dbContext.CompanyCache.Add(cacheEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("PK_CompanyCache") == true)
|
||||||
|
{
|
||||||
|
// Race condition: another task already cached this company - ignore
|
||||||
|
_logger.LogDebug("Company {CompanyNumber} already cached by another task", item.CompanyNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompanyVerificationResult CreateResultFromCache(
|
private CompanyVerificationResult CreateResultFromCache(
|
||||||
@@ -623,13 +633,22 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
|||||||
string? jobTitle,
|
string? jobTitle,
|
||||||
List<CompanyVerificationFlag> flags)
|
List<CompanyVerificationFlag> flags)
|
||||||
{
|
{
|
||||||
var matchScore = Fuzz.Ratio(
|
var matchScore = Fuzz.TokenSetRatio(
|
||||||
claimedCompany.ToUpperInvariant(),
|
claimedCompany.ToUpperInvariant(),
|
||||||
cached.CompanyName.ToUpperInvariant());
|
cached.CompanyName.ToUpperInvariant());
|
||||||
|
|
||||||
var sicCodes = !string.IsNullOrEmpty(cached.SicCodesJson)
|
List<string>? sicCodes = null;
|
||||||
? JsonSerializer.Deserialize<List<string>>(cached.SicCodesJson)
|
if (!string.IsNullOrEmpty(cached.SicCodesJson))
|
||||||
: null;
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sicCodes = JsonSerializer.Deserialize<List<string>>(cached.SicCodesJson);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
// Ignore malformed JSON in cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run all verification checks
|
// Run all verification checks
|
||||||
CheckIncorporationDate(flags, startDate, cached.IncorporationDate, cached.CompanyName);
|
CheckIncorporationDate(flags, startDate, cached.IncorporationDate, cached.CompanyName);
|
||||||
@@ -657,7 +676,7 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
|||||||
MatchedCompanyNumber = cached.CompanyNumber,
|
MatchedCompanyNumber = cached.CompanyNumber,
|
||||||
MatchScore = matchScore,
|
MatchScore = matchScore,
|
||||||
IsVerified = true,
|
IsVerified = true,
|
||||||
VerificationNotes = $"Matched from cache with {matchScore}% confidence",
|
VerificationNotes = null,
|
||||||
ClaimedStartDate = startDate,
|
ClaimedStartDate = startDate,
|
||||||
ClaimedEndDate = endDate,
|
ClaimedEndDate = endDate,
|
||||||
CompanyType = cached.CompanyType,
|
CompanyType = cached.CompanyType,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
@inject ILogger<Dashboard> Logger
|
@inject ILogger<Dashboard> Logger
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject TrueCV.Web.Services.PdfReportService PdfReportService
|
@inject TrueCV.Web.Services.PdfReportService PdfReportService
|
||||||
|
@inject IAuditService AuditService
|
||||||
|
|
||||||
<PageTitle>Dashboard - TrueCV</PageTitle>
|
<PageTitle>Dashboard - TrueCV</PageTitle>
|
||||||
|
|
||||||
@@ -179,7 +180,7 @@
|
|||||||
case "Processing":
|
case "Processing":
|
||||||
<span class="badge bg-primary">
|
<span class="badge bg-primary">
|
||||||
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true" style="width: 0.7rem; height: 0.7rem;"></span>
|
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true" style="width: 0.7rem; height: 0.7rem;"></span>
|
||||||
Processing
|
@(check.ProcessingStage ?? "Processing")
|
||||||
</span>
|
</span>
|
||||||
break;
|
break;
|
||||||
case "Pending":
|
case "Pending":
|
||||||
@@ -206,24 +207,32 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
@if (check.Status == "Completed")
|
<div class="btn-group" role="group">
|
||||||
{
|
@if (check.Status == "Completed")
|
||||||
<a href="/report/@check.Id" class="btn btn-sm btn-outline-primary" @onclick:stopPropagation="true">
|
{
|
||||||
View Report
|
<a href="/report/@check.Id" class="btn btn-sm btn-outline-primary" @onclick:stopPropagation="true">
|
||||||
</a>
|
View Report
|
||||||
}
|
</a>
|
||||||
else if (check.Status is "Pending" or "Processing")
|
}
|
||||||
{
|
else if (check.Status is "Pending" or "Processing")
|
||||||
<button class="btn btn-sm btn-outline-secondary" disabled>
|
{
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<button class="btn btn-sm btn-outline-secondary" disabled>
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/check" class="btn btn-sm btn-outline-warning" @onclick:stopPropagation="true">
|
||||||
|
Retry
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteCheck(check.Id)" @onclick:stopPropagation="true" title="Delete">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
|
||||||
|
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
}
|
</div>
|
||||||
else
|
|
||||||
{
|
|
||||||
<a href="/check" class="btn btn-sm btn-outline-warning">
|
|
||||||
Retry
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@@ -252,7 +261,9 @@
|
|||||||
private string? _errorMessage;
|
private string? _errorMessage;
|
||||||
private Guid _userId;
|
private Guid _userId;
|
||||||
private System.Threading.Timer? _pollingTimer;
|
private System.Threading.Timer? _pollingTimer;
|
||||||
private bool _isPolling;
|
private volatile bool _isPolling;
|
||||||
|
private volatile bool _disposed;
|
||||||
|
private volatile bool _isOperationInProgress;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -262,20 +273,31 @@
|
|||||||
|
|
||||||
private void StartPollingIfNeeded()
|
private void StartPollingIfNeeded()
|
||||||
{
|
{
|
||||||
if (HasProcessingChecks() && !_isPolling)
|
if (HasProcessingChecks() && !_isPolling && !_disposed)
|
||||||
{
|
{
|
||||||
_isPolling = true;
|
_isPolling = true;
|
||||||
_pollingTimer = new System.Threading.Timer(async _ =>
|
_pollingTimer = new System.Threading.Timer(async _ =>
|
||||||
{
|
{
|
||||||
await InvokeAsync(async () =>
|
if (_disposed) return;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await LoadChecks();
|
await InvokeAsync(async () =>
|
||||||
if (!HasProcessingChecks())
|
|
||||||
{
|
{
|
||||||
StopPolling();
|
if (_disposed) return;
|
||||||
}
|
|
||||||
StateHasChanged();
|
await LoadChecks();
|
||||||
});
|
if (!HasProcessingChecks())
|
||||||
|
{
|
||||||
|
StopPolling();
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// Component was disposed, ignore
|
||||||
|
}
|
||||||
}, null, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3));
|
}, null, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,11 +320,15 @@
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
_disposed = true;
|
||||||
StopPolling();
|
StopPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadChecks()
|
private async Task LoadChecks()
|
||||||
{
|
{
|
||||||
|
if (_isOperationInProgress) return;
|
||||||
|
|
||||||
|
_isOperationInProgress = true;
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
|
|
||||||
@@ -317,7 +343,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_checks = await CVCheckService.GetUserChecksAsync(_userId);
|
_checks = await CVCheckService.GetUserChecksAsync(_userId) ?? [];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -327,6 +353,7 @@
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
|
_isOperationInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,6 +427,8 @@
|
|||||||
var base64 = Convert.ToBase64String(pdfBytes);
|
var base64 = Convert.ToBase64String(pdfBytes);
|
||||||
var fileName = "TrueCV_Report_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".pdf";
|
var fileName = "TrueCV_Report_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".pdf";
|
||||||
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, "application/pdf");
|
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, "application/pdf");
|
||||||
|
|
||||||
|
await AuditService.LogAsync(_userId, AuditActions.ReportExported, null, null, $"Exported {reportDataList.Count} reports to PDF");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -420,4 +449,29 @@
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task DeleteCheck(Guid checkId)
|
||||||
|
{
|
||||||
|
if (_isOperationInProgress) return;
|
||||||
|
|
||||||
|
_isOperationInProgress = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var success = await CVCheckService.DeleteCheckAsync(checkId, _userId);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
_checks.RemoveAll(c => c.Id == checkId);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error deleting CV check {CheckId}", checkId);
|
||||||
|
_errorMessage = "Failed to delete the CV check. Please try again.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isOperationInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||||
@inject ILogger<Report> Logger
|
@inject ILogger<Report> Logger
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject IAuditService AuditService
|
||||||
|
|
||||||
<PageTitle>Verification Report - TrueCV</PageTitle>
|
<PageTitle>Verification Report - TrueCV</PageTitle>
|
||||||
|
|
||||||
@@ -546,6 +547,10 @@
|
|||||||
{
|
{
|
||||||
_errorMessage = "Unable to load the report data.";
|
_errorMessage = "Unable to load the report data.";
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await AuditService.LogAsync(_userId, AuditActions.ReportViewed, "CVCheck", Id, $"Score: {_report.OverallScore}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public sealed class ProcessCVCheckJobTests : IDisposable
|
|||||||
private readonly Mock<ICompanyVerifierService> _companyVerifierServiceMock;
|
private readonly Mock<ICompanyVerifierService> _companyVerifierServiceMock;
|
||||||
private readonly Mock<IEducationVerifierService> _educationVerifierServiceMock;
|
private readonly Mock<IEducationVerifierService> _educationVerifierServiceMock;
|
||||||
private readonly Mock<ITimelineAnalyserService> _timelineAnalyserServiceMock;
|
private readonly Mock<ITimelineAnalyserService> _timelineAnalyserServiceMock;
|
||||||
|
private readonly Mock<IAuditService> _auditServiceMock;
|
||||||
private readonly Mock<ILogger<ProcessCVCheckJob>> _loggerMock;
|
private readonly Mock<ILogger<ProcessCVCheckJob>> _loggerMock;
|
||||||
private readonly ProcessCVCheckJob _sut;
|
private readonly ProcessCVCheckJob _sut;
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ public sealed class ProcessCVCheckJobTests : IDisposable
|
|||||||
_companyVerifierServiceMock = new Mock<ICompanyVerifierService>();
|
_companyVerifierServiceMock = new Mock<ICompanyVerifierService>();
|
||||||
_educationVerifierServiceMock = new Mock<IEducationVerifierService>();
|
_educationVerifierServiceMock = new Mock<IEducationVerifierService>();
|
||||||
_timelineAnalyserServiceMock = new Mock<ITimelineAnalyserService>();
|
_timelineAnalyserServiceMock = new Mock<ITimelineAnalyserService>();
|
||||||
|
_auditServiceMock = new Mock<IAuditService>();
|
||||||
_loggerMock = new Mock<ILogger<ProcessCVCheckJob>>();
|
_loggerMock = new Mock<ILogger<ProcessCVCheckJob>>();
|
||||||
|
|
||||||
_sut = new ProcessCVCheckJob(
|
_sut = new ProcessCVCheckJob(
|
||||||
@@ -49,6 +51,7 @@ public sealed class ProcessCVCheckJobTests : IDisposable
|
|||||||
_companyVerifierServiceMock.Object,
|
_companyVerifierServiceMock.Object,
|
||||||
_educationVerifierServiceMock.Object,
|
_educationVerifierServiceMock.Object,
|
||||||
_timelineAnalyserServiceMock.Object,
|
_timelineAnalyserServiceMock.Object,
|
||||||
|
_auditServiceMock.Object,
|
||||||
_loggerMock.Object);
|
_loggerMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public sealed class CVCheckServiceTests : IDisposable
|
|||||||
private readonly ApplicationDbContext _dbContext;
|
private readonly ApplicationDbContext _dbContext;
|
||||||
private readonly Mock<IFileStorageService> _fileStorageServiceMock;
|
private readonly Mock<IFileStorageService> _fileStorageServiceMock;
|
||||||
private readonly Mock<IBackgroundJobClient> _backgroundJobClientMock;
|
private readonly Mock<IBackgroundJobClient> _backgroundJobClientMock;
|
||||||
|
private readonly Mock<IAuditService> _auditServiceMock;
|
||||||
private readonly Mock<ILogger<CVCheckService>> _loggerMock;
|
private readonly Mock<ILogger<CVCheckService>> _loggerMock;
|
||||||
private readonly CVCheckService _sut;
|
private readonly CVCheckService _sut;
|
||||||
|
|
||||||
@@ -35,12 +36,14 @@ public sealed class CVCheckServiceTests : IDisposable
|
|||||||
_dbContext = new ApplicationDbContext(options);
|
_dbContext = new ApplicationDbContext(options);
|
||||||
_fileStorageServiceMock = new Mock<IFileStorageService>();
|
_fileStorageServiceMock = new Mock<IFileStorageService>();
|
||||||
_backgroundJobClientMock = new Mock<IBackgroundJobClient>();
|
_backgroundJobClientMock = new Mock<IBackgroundJobClient>();
|
||||||
|
_auditServiceMock = new Mock<IAuditService>();
|
||||||
_loggerMock = new Mock<ILogger<CVCheckService>>();
|
_loggerMock = new Mock<ILogger<CVCheckService>>();
|
||||||
|
|
||||||
_sut = new CVCheckService(
|
_sut = new CVCheckService(
|
||||||
_dbContext,
|
_dbContext,
|
||||||
_fileStorageServiceMock.Object,
|
_fileStorageServiceMock.Object,
|
||||||
_backgroundJobClientMock.Object,
|
_backgroundJobClientMock.Object,
|
||||||
|
_auditServiceMock.Object,
|
||||||
_loggerMock.Object);
|
_loggerMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user