using Microsoft.EntityFrameworkCore; namespace GreenHome.Infrastructure.Data; public sealed class GreenHomeDbContext : DbContext { public GreenHomeDbContext(DbContextOptions options) : base(options) { } public DbSet Devices => Set(); public DbSet TelemetryRecords => Set(); public DbSet DeviceSettings => Set(); public DbSet AlertConditions => Set(); public DbSet AlertRules => Set(); public DbSet DeviceRelays => Set(); public DbSet RelayRuleGroups => Set(); public DbSet RelayRuleConditions => Set(); public DbSet Users => Set(); public DbSet VerificationCodes => Set(); public DbSet DeviceUsers => Set(); public DbSet AlertNotifications => Set(); public DbSet AlertLogs => Set(); public DbSet AIQueries => Set(); public DbSet AIProfiles => Set(); public DbSet DailyReports => Set(); public DbSet UserDailyReports => Set(); public DbSet ReportImages => Set(); public DbSet Checklists => Set(); public DbSet ChecklistItems => Set(); public DbSet ChecklistCompletions => Set(); public DbSet ChecklistItemCompletions => Set(); public DbSet DevicePosts => Set(); public DbSet DevicePostImages => Set(); public DbSet PowerOutageLogs => Set(); public DbSet UsageCounters => Set(); public DbSet AdminAuditLogs => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity(b => { b.ToTable("Devices"); b.HasKey(x => x.Id); b.Property(x => x.DeviceName).IsRequired().HasMaxLength(10); b.Property(x => x.UserId).IsRequired(); b.Property(x => x.Location).HasMaxLength(250); b.Property(x => x.NeshanLocation).HasMaxLength(80); b.HasOne(x => x.User) .WithMany() .HasForeignKey(x => x.UserId) .OnDelete(DeleteBehavior.Restrict); }); modelBuilder.Entity(b => { b.ToTable("Telemetry"); b.HasKey(x => x.Id); b.Property(x => x.TemperatureC).HasColumnType("decimal(18,2)"); b.Property(x => x.HumidityPercent).HasColumnType("decimal(18,2)"); b.Property(x => x.SoilPercent).HasColumnType("decimal(18,2)"); b.Property(x => x.Lux).HasColumnType("decimal(18,2)"); b.Property(x => x.PersianDate).HasMaxLength(10); b.Property(x => x.Voltage).HasColumnType("decimal(18,2)"); b.Property(x => x.BatteryVoltage).HasColumnType("decimal(18,2)"); b.Property(x => x.BatteryPercent).HasColumnType("decimal(18,2)"); b.Property(x => x.RelayStates).HasMaxLength(64); b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth }); b.HasIndex(x => new { x.DeviceId, x.TimestampUtc }); b.HasIndex(x => new { x.DeviceId, x.ServerTimestampUtc }); }); modelBuilder.Entity(b => { b.ToTable("DeviceSettings"); b.HasKey(x => x.Id); b.Property(x => x.Province).HasMaxLength(100); b.Property(x => x.City).HasMaxLength(100); b.Property(x => x.Latitude).HasColumnType("decimal(9,6)"); b.Property(x => x.Longitude).HasColumnType("decimal(9,6)"); b.Property(x => x.ProductType).HasMaxLength(100); b.Property(x => x.MinimumSmsIntervalMinutes).HasDefaultValue(15); b.Property(x => x.MinimumCallIntervalMinutes).HasDefaultValue(60); b.Property(x => x.AreaSquareMeters).HasColumnType("decimal(18,2)"); b.Property(x => x.SmsRecievers).HasMaxLength(120); b.Property(x => x.RelayCount).HasDefaultValue(4); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => x.DeviceId).IsUnique(); b.HasMany(x => x.Relays) .WithOne(r => r.DeviceSettings) .HasForeignKey(r => r.DeviceSettingsId) .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity(b => { b.ToTable("DeviceRelays"); b.HasKey(x => x.Id); b.Property(x => x.RelayIndex).IsRequired(); b.Property(x => x.Name).HasMaxLength(100); b.Property(x => x.RelayKind).IsRequired().HasConversion(); b.HasIndex(x => new { x.DeviceSettingsId, x.RelayIndex }).IsUnique(); }); modelBuilder.Entity(b => { b.ToTable("RelayRuleGroups"); b.HasKey(x => x.Id); b.Property(x => x.Action).IsRequired().HasConversion(); b.Property(x => x.Label).HasMaxLength(200); b.Property(x => x.Order).IsRequired(); b.HasOne(x => x.DeviceRelay) .WithMany(r => r.RuleGroups) .HasForeignKey(x => x.DeviceRelayId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => x.DeviceRelayId); }); modelBuilder.Entity(b => { b.ToTable("RelayRuleConditions"); b.HasKey(x => x.Id); b.Property(x => x.ConditionType).IsRequired().HasConversion(); b.Property(x => x.SensorType).HasConversion(); b.Property(x => x.ComparisonType).HasConversion(); b.Property(x => x.Value1).HasColumnType("decimal(18,2)"); b.Property(x => x.Value2).HasColumnType("decimal(18,2)"); b.Property(x => x.Order).IsRequired(); b.HasOne(x => x.RelayRuleGroup) .WithMany(g => g.Conditions) .HasForeignKey(x => x.RelayRuleGroupId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => x.RelayRuleGroupId); }); modelBuilder.Entity(b => { b.ToTable("AlertConditions"); b.HasKey(x => x.Id); b.Property(x => x.NotificationType).IsRequired().HasConversion(); b.Property(x => x.TimeType).IsRequired().HasConversion(); b.Property(x => x.CallCooldownMinutes).IsRequired(); b.Property(x => x.SmsCooldownMinutes).IsRequired(); b.Property(x => x.IsEnabled).IsRequired(); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => x.DeviceId); }); modelBuilder.Entity(b => { b.ToTable("AlertRules"); b.HasKey(x => x.Id); b.Property(x => x.SensorType).IsRequired().HasConversion(); b.Property(x => x.ComparisonType).IsRequired().HasConversion(); b.Property(x => x.Value1).IsRequired().HasColumnType("decimal(18,2)"); b.Property(x => x.Value2).HasColumnType("decimal(18,2)"); b.Property(x => x.Order).IsRequired(); b.HasOne(x => x.AlertCondition) .WithMany(c => c.Rules) .HasForeignKey(x => x.AlertConditionId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => x.AlertConditionId); }); modelBuilder.Entity(b => { b.ToTable("Users"); b.HasKey(x => x.Id); b.Property(x => x.Mobile).IsRequired().HasMaxLength(11); b.Property(x => x.Name).IsRequired().HasMaxLength(100); b.Property(x => x.Family).IsRequired().HasMaxLength(100); b.Property(x => x.Role).IsRequired().HasConversion(); b.HasIndex(x => x.Mobile).IsUnique(); }); modelBuilder.Entity(b => { b.ToTable("DeviceUsers"); b.HasKey(x => new { x.DeviceId, x.UserId }); b.Property(x => x.ReceiveAlerts).IsRequired().HasDefaultValue(true); b.Property(x => x.Role).IsRequired().HasConversion(); b.Property(x => x.Name).IsRequired().HasMaxLength(100); b.Property(x => x.Family).IsRequired().HasMaxLength(100); b.Property(x => x.AvatarPath).HasMaxLength(500); b.HasOne(x => x.Device) .WithMany(d => d.DeviceUsers) .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.Cascade); b.HasOne(x => x.User) .WithMany(u => u.DeviceUsers) .HasForeignKey(x => x.UserId) .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity(b => { b.ToTable("VerificationCodes"); b.HasKey(x => x.Id); b.Property(x => x.Mobile).IsRequired().HasMaxLength(11); b.Property(x => x.Code).IsRequired().HasMaxLength(4); b.HasIndex(x => new { x.Mobile, x.Code, x.IsUsed }); }); modelBuilder.Entity(b => { b.ToTable("AlertNotifications"); b.HasKey(x => x.Id); b.Property(x => x.NotificationType).IsRequired().HasConversion(); b.Property(x => x.Message).IsRequired().HasMaxLength(500); b.Property(x => x.MessageOutboxIds).HasMaxLength(500); b.Property(x => x.ErrorMessage).HasMaxLength(1000); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.Restrict); b.HasOne(x => x.User) .WithMany() .HasForeignKey(x => x.UserId) .OnDelete(DeleteBehavior.Restrict); b.HasOne(x => x.AlertCondition) .WithMany() .HasForeignKey(x => x.AlertConditionId) .OnDelete(DeleteBehavior.Restrict); b.HasIndex(x => new { x.DeviceId, x.UserId, x.AlertConditionId, x.SentAt }); }); modelBuilder.Entity(b => { b.ToTable("AIQueries"); b.HasKey(x => x.Id); b.Property(x => x.Question).IsRequired(); b.Property(x => x.Answer).IsRequired(); b.Property(x => x.Model).HasMaxLength(100); b.Property(x => x.CostUsd).HasColumnType("decimal(18,8)"); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.SetNull); b.HasOne(x => x.User) .WithMany() .HasForeignKey(x => x.UserId) .OnDelete(DeleteBehavior.SetNull); b.HasOne(x => x.Profile) .WithMany() .HasForeignKey(x => x.ProfileId) .OnDelete(DeleteBehavior.SetNull); b.HasIndex(x => x.DeviceId); b.HasIndex(x => x.UserId); b.HasIndex(x => x.ProfileId); b.HasIndex(x => x.CreatedAt); }); modelBuilder.Entity(b => { b.ToTable("AIProfiles"); b.HasKey(x => x.Id); b.Property(x => x.Name).IsRequired().HasMaxLength(100); b.Property(x => x.Provider).IsRequired().HasConversion(); b.Property(x => x.BaseUrl).IsRequired().HasMaxLength(300); b.Property(x => x.ApiKey).IsRequired().HasMaxLength(500); b.Property(x => x.Model).IsRequired().HasMaxLength(100); b.Property(x => x.PromptPricePer1M).HasColumnType("decimal(18,6)"); b.Property(x => x.CompletionPricePer1M).HasColumnType("decimal(18,6)"); b.Property(x => x.BalanceUsd).HasColumnType("decimal(18,6)"); b.Property(x => x.BalanceCurrency).HasMaxLength(10); b.HasIndex(x => x.IsActive); }); modelBuilder.Entity(b => { b.ToTable("DailyReports"); b.HasKey(x => x.Id); b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10); b.Property(x => x.Analysis).IsRequired(); b.Property(x => x.Model).HasMaxLength(100); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => new { x.DeviceId, x.PersianDate }).IsUnique(); b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth }); b.HasIndex(x => x.CreatedAt); }); modelBuilder.Entity(b => { b.ToTable("AlertLogs"); b.HasKey(x => x.Id); b.Property(x => x.AlertType).IsRequired().HasConversion(); b.Property(x => x.NotificationType).IsRequired().HasConversion(); b.Property(x => x.Message).IsRequired().HasMaxLength(1000); b.Property(x => x.Status).IsRequired().HasConversion(); b.Property(x => x.ErrorMessage).HasMaxLength(2000); b.Property(x => x.PhoneNumber).IsRequired().HasMaxLength(20); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.Restrict); b.HasOne(x => x.User) .WithMany() .HasForeignKey(x => x.UserId) .OnDelete(DeleteBehavior.Restrict); b.HasOne(x => x.AlertCondition) .WithMany() .HasForeignKey(x => x.AlertConditionId) .OnDelete(DeleteBehavior.SetNull); b.HasIndex(x => new { x.DeviceId, x.SentAt }); b.HasIndex(x => new { x.UserId, x.SentAt }); b.HasIndex(x => x.AlertType); b.HasIndex(x => x.Status); }); modelBuilder.Entity(b => { b.ToTable("UserDailyReports"); b.HasKey(x => x.Id); b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10); b.Property(x => x.Title).IsRequired().HasMaxLength(200); b.Property(x => x.Observations).IsRequired(); b.Property(x => x.Operations).IsRequired(); b.Property(x => x.Notes).HasMaxLength(2000); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.Cascade); b.HasOne(x => x.User) .WithMany() .HasForeignKey(x => x.UserId) .OnDelete(DeleteBehavior.Restrict); b.HasIndex(x => new { x.DeviceId, x.PersianDate }); b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth }); b.HasIndex(x => x.UserId); }); modelBuilder.Entity(b => { b.ToTable("ReportImages"); b.HasKey(x => x.Id); b.Property(x => x.FileName).IsRequired().HasMaxLength(255); b.Property(x => x.FilePath).IsRequired().HasMaxLength(500); b.Property(x => x.ContentType).IsRequired().HasMaxLength(100); b.Property(x => x.Description).HasMaxLength(500); b.HasOne(x => x.UserDailyReport) .WithMany(r => r.Images) .HasForeignKey(x => x.UserDailyReportId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => x.UserDailyReportId); }); modelBuilder.Entity(b => { b.ToTable("Checklists"); b.HasKey(x => x.Id); b.Property(x => x.Title).IsRequired().HasMaxLength(200); b.Property(x => x.Description).HasMaxLength(1000); b.Property(x => x.IsActive).IsRequired(); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.Cascade); b.HasOne(x => x.CreatedByUser) .WithMany() .HasForeignKey(x => x.CreatedByUserId) .OnDelete(DeleteBehavior.Restrict); b.HasIndex(x => new { x.DeviceId, x.IsActive }); }); modelBuilder.Entity(b => { b.ToTable("ChecklistItems"); b.HasKey(x => x.Id); b.Property(x => x.Title).IsRequired().HasMaxLength(300); b.Property(x => x.Description).HasMaxLength(1000); b.Property(x => x.Order).IsRequired(); b.Property(x => x.IsRequired).IsRequired(); b.HasOne(x => x.Checklist) .WithMany(c => c.Items) .HasForeignKey(x => x.ChecklistId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => new { x.ChecklistId, x.Order }); }); modelBuilder.Entity(b => { b.ToTable("ChecklistCompletions"); b.HasKey(x => x.Id); b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10); b.Property(x => x.Notes).HasMaxLength(2000); b.HasOne(x => x.Checklist) .WithMany(c => c.Completions) .HasForeignKey(x => x.ChecklistId) .OnDelete(DeleteBehavior.Cascade); b.HasOne(x => x.CompletedByUser) .WithMany() .HasForeignKey(x => x.CompletedByUserId) .OnDelete(DeleteBehavior.Restrict); b.HasIndex(x => new { x.ChecklistId, x.PersianDate }); b.HasIndex(x => x.CompletedByUserId); }); modelBuilder.Entity(b => { b.ToTable("ChecklistItemCompletions"); b.HasKey(x => x.Id); b.Property(x => x.IsChecked).IsRequired(); b.Property(x => x.Note).HasMaxLength(500); b.HasOne(x => x.ChecklistCompletion) .WithMany(cc => cc.ItemCompletions) .HasForeignKey(x => x.ChecklistCompletionId) .OnDelete(DeleteBehavior.Cascade); b.HasOne(x => x.ChecklistItem) .WithMany() .HasForeignKey(x => x.ChecklistItemId) .OnDelete(DeleteBehavior.Restrict); b.HasIndex(x => x.ChecklistCompletionId); }); modelBuilder.Entity(b => { b.ToTable("DevicePosts"); b.HasKey(x => x.Id); b.Property(x => x.Content).IsRequired().HasMaxLength(5000); b.Property(x => x.PostType).IsRequired().HasConversion(); b.Property(x => x.VoiceFilePath).HasMaxLength(500); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.Cascade); b.HasOne(x => x.AuthorUser) .WithMany() .HasForeignKey(x => x.AuthorUserId) .OnDelete(DeleteBehavior.Restrict); b.HasIndex(x => new { x.DeviceId, x.CreatedAt }); b.HasIndex(x => x.AuthorUserId); }); modelBuilder.Entity(b => { b.ToTable("DevicePostImages"); b.HasKey(x => x.Id); b.Property(x => x.FileName).IsRequired().HasMaxLength(255); b.Property(x => x.FilePath).IsRequired().HasMaxLength(500); b.Property(x => x.ContentType).IsRequired().HasMaxLength(100); b.HasOne(x => x.DevicePost) .WithMany(p => p.Images) .HasForeignKey(x => x.DevicePostId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => x.DevicePostId); }); modelBuilder.Entity(b => { b.ToTable("PowerOutageLogs"); b.HasKey(x => x.Id); b.Property(x => x.PreviousPower).IsRequired(); b.Property(x => x.CurrentPower).IsRequired(); b.Property(x => x.EventType).IsRequired().HasConversion(); b.Property(x => x.PersianDate).HasMaxLength(10); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => new { x.DeviceId, x.TimestampUtc }); b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth }); b.HasIndex(x => x.EventType); }); modelBuilder.Entity(b => { b.ToTable("UsageCounters"); b.HasKey(x => x.Id); b.HasOne(x => x.User) .WithMany() .HasForeignKey(x => x.UserId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => new { x.UserId, x.Date }).IsUnique(); b.HasIndex(x => x.Date); }); modelBuilder.Entity(b => { b.ToTable("AdminAuditLogs"); b.HasKey(x => x.Id); b.Property(x => x.Action).IsRequired().HasMaxLength(100); b.Property(x => x.TargetId).IsRequired().HasMaxLength(100); b.Property(x => x.Details).HasMaxLength(2000); b.HasOne(x => x.AdminUser) .WithMany() .HasForeignKey(x => x.AdminUserId) .OnDelete(DeleteBehavior.Restrict); b.HasIndex(x => x.At); b.HasIndex(x => x.AdminUserId); }); // Postgres-specific mappings (SQL Server path is untouched). // MUST run last: fluent HasMaxLength/HasColumnType calls above set the // values we adjust here, so this block has to see the final model state. // 1) DateTime -> "timestamp" (no tz): mirrors naive datetime2, accepts any Kind. // 2) varchar: Postgres counts BYTES, SQL Server's nvarchar counts CHARACTERS. // x4 keeps char-count semantics for any UTF-8 charset (Persian = 2 bytes/char). if (Database.ProviderName == "Npgsql.EntityFrameworkCore.PostgreSQL") { foreach (var et in modelBuilder.Model.GetEntityTypes()) { foreach (var p in et.GetProperties()) { if (p.ClrType == typeof(DateTime) || p.ClrType == typeof(DateTime?)) p.SetColumnType("timestamp"); else if (p.ClrType == typeof(string) && p.GetMaxLength() is int len) p.SetColumnType($"character varying({len * 4})"); } } } } }