using Microsoft.EntityFrameworkCore; using MinAttest.Domain.Entities; using MinAttest.Application.Abstractions; namespace MinAttest.Infrastructure.Data; public class AppDbContext(DbContextOptions options) : DbContext(options), IAppDbContext { public DbSet Persons => Set(); public DbSet Employers => Set(); public DbSet Attests => Set(); public DbSet ShareLinks => Set(); public DbSet AuditLogs => Set(); public DbSet EmployerUsers => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(b => { b.HasKey(x => x.Id); b.Property(x => x.NationalIdHash).HasMaxLength(200); b.HasIndex(x => x.NationalIdHash); b.ToTable(t => t.IsTemporal(tt => tt .UseHistoryTable("PersonsHistory") )); }); modelBuilder.Entity(b => { b.HasKey(x => x.Id); b.Property(x => x.OrgNumber).HasMaxLength(32); b.HasIndex(x => x.OrgNumber).IsUnique(false); b.ToTable(t => t.IsTemporal(tt => tt .UseHistoryTable("EmployersHistory") )); }); modelBuilder.Entity(b => { b.HasKey(x => x.Id); b.Property(x => x.Title).HasMaxLength(200); b.Property(x => x.Summary).HasMaxLength(2000); b.Property(x => x.BlobPath).HasMaxLength(500); b.Property(x => x.BlobHash).HasMaxLength(128); b.Property(x => x.Content).HasColumnType("varbinary(max)"); b.Property(x => x.ContentType).HasMaxLength(255); b.Property(x => x.ContentLength).HasColumnType("bigint"); b.HasOne(x => x.Person) .WithMany(x => x.Attests) .HasForeignKey(x => x.PersonId) .OnDelete(DeleteBehavior.Cascade); b.HasOne(x => x.Employer) .WithMany(x => x.Attests) .HasForeignKey(x => x.EmployerId) .OnDelete(DeleteBehavior.Restrict); // Consistency: EmployerId null <=> Unverified, EmployerId set => Issued/Revoked b.ToTable(t => { t.HasCheckConstraint( "CK_Attests_Verification", "(([EmployerId] IS NULL AND [Status] = 2) OR ([EmployerId] IS NOT NULL AND [Status] IN (1,3)))"); t.IsTemporal(tt => tt.UseHistoryTable("AttestsHistory")); }); // Helpful composite indexes for common queries b.HasIndex(x => new { x.PersonId, x.Status }); b.HasIndex(x => new { x.EmployerId, x.Status }); }); modelBuilder.Entity(b => { b.HasKey(x => x.Id); b.Property(x => x.Code).HasMaxLength(200); b.HasIndex(x => x.Code).IsUnique(); b.HasOne(x => x.Attest) .WithMany(x => x.ShareLinks) .HasForeignKey(x => x.AttestId) .OnDelete(DeleteBehavior.Cascade); b.ToTable(t => t.IsTemporal(tt => tt .UseHistoryTable("ShareLinksHistory") )); }); modelBuilder.Entity(b => { b.HasKey(x => x.Id); b.Property(x => x.ExternalObjectId).HasMaxLength(128); b.Property(x => x.Email).HasMaxLength(256); b.Property(x => x.Name).HasMaxLength(200); b.HasIndex(x => x.ExternalObjectId).IsUnique(); b.HasIndex(x => x.EmployerId); b.HasOne(x => x.Employer) .WithMany(e => e.Users) .HasForeignKey(x => x.EmployerId) .OnDelete(DeleteBehavior.Cascade); b.ToTable(t => t.IsTemporal(tt => tt .UseHistoryTable("EmployerUsersHistory") )); }); modelBuilder.Entity(b => { b.HasKey(x => x.Id); b.Property(x => x.Action).HasMaxLength(100); b.Property(x => x.TargetType).HasMaxLength(100); b.Property(x => x.Ip).HasMaxLength(64); b.HasIndex(x => x.Timestamp); }); } public override int SaveChanges(bool acceptAllChangesOnSuccess) { NormalizeAttestStatuses(); return base.SaveChanges(acceptAllChangesOnSuccess); } public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { NormalizeAttestStatuses(); return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } private void NormalizeAttestStatuses() { foreach (var entry in ChangeTracker.Entries()) { if (entry.State is not (EntityState.Added or EntityState.Modified)) continue; var a = entry.Entity; if (a.EmployerId is null) { // No employer -> always Unverified a.Status = AttestStatus.Unverified; } else if (a.Status == AttestStatus.Unverified) { // Has employer but Unverified -> default to Issued a.Status = AttestStatus.Issued; } } } }