using Fitness_core_BE.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; namespace Fitness_core_BE { // Central database: data that is inherently cross-tenant -- customer identity, the // client/gym registry, admin data, admin-authored plan templates, and platform billing. // See docs/AppDBContext split rationale in the migration plan. Row-split entities // (MealPlan/WorkoutPlan family, PaymentTransactions) also live here for template / // platform-billing rows -- TenantDbContext holds the same entity types for the // client-owned / member-payment rows. public class CentralDbContext : DbContext { public CentralDbContext(DbContextOptions options) : base(options) { } // Identity & tenant registry public DbSet Users => Set(); public DbSet Admins => Set(); public DbSet Customer => Set(); public DbSet Client => Set(); public DbSet ClientBusiness => Set(); // Agent/PairingCode stay central: Agent authenticates purely via X-Api-Key (no JWT at // all), and PairingCode is created by platform Admin and consumed by an anonymous agent // enroll call -- neither has any tenant-scoping mechanism to key off of. public DbSet Agents => Set(); public DbSet PairingCodes => Set(); // Background-job run history (SubscriptionRenewalJob, etc.) -- central because it's // not gym-operational data, just an audit trail of what each sweep did. public DbSet JobRunLogs => Set(); // Customer-follows-you-everywhere data public DbSet CustomerHealth => Set(); public DbSet CustomerTargets => Set(); public DbSet CusWeightLossProgress => Set(); public DbSet CustomerNotifications => Set(); // Admin-authored plan templates (IsTemplate=true / FKClientId=null rows only) public DbSet MealCategories => Set(); public DbSet MealPlans => Set(); public DbSet Meal => Set(); public DbSet WorkoutCategories => Set(); public DbSet WorkoutPlans => Set(); public DbSet WorkoutItems => Set(); public DbSet WorkoutExercises => Set(); // Support / global catalog public DbSet Tickets => Set(); public DbSet TicketCategories => Set(); public DbSet TicketMessages => Set(); public DbSet NavItems => Set(); public DbSet UserDocuments => Set(); public DbSet DocumentTypes => Set(); // Platform billing (HexaFitness -> gym) public DbSet ClientInvoices => Set(); public DbSet ClientCard => Set(); public DbSet ClientInvoiceItems => Set(); public DbSet ClientSubscriptions => Set(); public DbSet ClientSubItems => Set(); public DbSet ClientPackages => Set(); public DbSet ClientPayoutKycs => Set(); public DbSet ClientWallets => Set(); public DbSet WalletLedgerEntries => Set(); public DbSet PayoutBatches => Set(); public DbSet DevicePricingConfigs => Set(); public DbSet ClientDeviceBillings => Set(); // Row-split: only rows with FK_ClientInvoiceId set (platform billing payments) live // here -- member payments (FK_CustomerInvoiceId set) live in TenantDbContext instead. public DbSet PaymentTransactions => Set(); // Ambiguous scope today (no FKClientId on the model) -- kept central pending the // Phase-2 confirmation spike called out in the migration plan. public DbSet FeedPosts => Set(); public DbSet FeedLikes => Set(); public DbSet ActivityLogs => Set(); public DbSet Chat => Set(); public DbSet ChatMessages => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Tenant-only entity types must never be part of this model at all. Ignoring // individual navigations (below) isn't enough on its own: an unignored tenant // collection/reference nav anywhere in the reachable graph (e.g. // CustomerSubscription.AutoChargeCard -> CustomerCards, CustomerInvoice. // CustomerSubscription) pulls the whole type into this context's model via EF's // reachability-based discovery, which can then invent a bogus shadow-FK // relationship between that type and whatever OTHER entity also happens to have // a real FK to it -- breaking queries with "Unknown column" once EF tries to // select a column that doesn't exist in the central schema. Same failure mode // proven and fixed on the TenantDbContext side (see its OnModelCreating) -- // mirrored here in the opposite direction. Row-split entities (MealPlan/ // WorkoutPlan family, PaymentTransaction) are deliberately NOT in this list -- // they have real DbSets in both contexts. modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Entity() .HasIndex(a => a.ApiKeyHash) .IsUnique(); modelBuilder.Entity() .HasIndex(r => new { r.JobName, r.RunAtUtc }); // Row-split entities: no FKClientId relationship enforced here (template rows // have FKClientId = null; the rare non-null value on a row that ended up in this // DB isn't a real cross-schema FK once tenants split out). modelBuilder.Entity().Ignore(p => p.Client); modelBuilder.Entity().Ignore(p => p.Client); modelBuilder.Entity().Ignore(p => p.Client); modelBuilder.Entity().Ignore(p => p.Customer); modelBuilder.Entity().Ignore(p => p.CustomerCard); modelBuilder.Entity().Ignore(p => p.CustomerInvoice); // Customer's collection navigations into tenant-only entities -- not resolvable // here; load those separately from TenantDbContext and merge in the caller. modelBuilder.Entity().Ignore(c => c.CustomerWorkouts); modelBuilder.Entity().Ignore(c => c.CustomerMeals); modelBuilder.Entity().Ignore(c => c.CustomerSubscriptions); modelBuilder.Entity().HasData( new NavItem { Key = "dashboard", Label = "Dashboard", ShortLabel = "Home", Href = "/dashboard", Icon = "LayoutDashboard", SortOrder = 0, IsOwnerOnly = false }, new NavItem { Key = "attendance", Label = "Attendance", Href = "/dashboard/attendance", Icon = "CalendarCheck", SortOrder = 1, IsOwnerOnly = false }, new NavItem { Key = "members", Label = "Members", Href = "/dashboard/members", Icon = "Users", SortOrder = 2, IsOwnerOnly = false }, new NavItem { Key = "memberships", Label = "Memberships", Href = "/dashboard/memberships", Icon = "CreditCard", SortOrder = 3, IsOwnerOnly = false }, new NavItem { Key = "bookings", Label = "Bookings", Href = "/dashboard/bookings", Icon = "CalendarClock", SortOrder = 4, IsOwnerOnly = false }, new NavItem { Key = "customer-payments", Label = "Customer Payments", ShortLabel = "Payments", Href = "/dashboard/customer-payments", Icon = "Receipt", SortOrder = 5, IsOwnerOnly = false }, new NavItem { Key = "inventory", Label = "Inventory", Href = "/dashboard/inventory", Icon = "Package", SortOrder = 6, IsOwnerOnly = false }, new NavItem { Key = "workout-plans", Label = "Workout Plans", Href = "/dashboard/workout-plans", Icon = "Activity", SortOrder = 7, IsOwnerOnly = false }, new NavItem { Key = "meal-plans", Label = "Meal Plans", Href = "/dashboard/meal-plans", Icon = "Utensils", SortOrder = 8, IsOwnerOnly = false }, new NavItem { Key = "staff", Label = "Staff", Href = "/dashboard/staff", Icon = "UsersRound", SortOrder = 9, IsOwnerOnly = false }, new NavItem { Key = "feed", Label = "Feed", Href = "/dashboard/feed", Icon = "Rss", SortOrder = 10, IsOwnerOnly = false }, new NavItem { Key = "tickets", Label = "Tickets", Href = "/dashboard/tickets", Icon = "TicketIcon", SortOrder = 11, IsOwnerOnly = false }, new NavItem { Key = "reports", Label = "Reports", Href = "/dashboard/reports", Icon = "BarChart3", SortOrder = 12, IsOwnerOnly = false }, new NavItem { Key = "settings", Label = "Gym Profile", Href = "/dashboard/settings", Icon = "Settings", SortOrder = 13, IsOwnerOnly = true } ); modelBuilder.Entity().HasData( new DocumentTypes { DocumentId = 1001, DocumentType = "NIC_FRONT" }, new DocumentTypes { DocumentId = 1002, DocumentType = "NIC_BACK" }, new DocumentTypes { DocumentId = 1003, DocumentType = "BR_CERTIFICATE" }, new DocumentTypes { DocumentId = 1004, DocumentType = "BANK_PROOF" }, // Images/video attached to a feed post -- FeedService.CreateFeed tags its // uploads with this id, so the row has to exist or the UserDocuments FK fails. new DocumentTypes { DocumentId = 1005, DocumentType = "FEED_POST" }, // Profile picture uploads (CustomerController/ClientController) tag with this id. new DocumentTypes { DocumentId = 1006, DocumentType = "PROFILE_IMAGE" } ); modelBuilder.Entity().HasData( new User { UserId = AIAgents.MealAgentId, UserName = "MealPlan AI", Email = "meal.ai@system.local", PhoneNumber = null, IsActive = true, CreateDate = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc) }, new User { UserId = AIAgents.WorkoutAgentId, UserName = "WorkoutPlan AI", Email = "workout.ai@system.local", PhoneNumber = null, IsActive = true, CreateDate = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc) } ); // Platform-wide "coach guided" suggested workout plans -- admin templates. modelBuilder.Entity().HasData(Data.WorkoutPlanSeedData.Categories()); modelBuilder.Entity().HasData(Data.WorkoutPlanSeedData.Plans()); modelBuilder.Entity().HasData(Data.WorkoutPlanSeedData.Items()); modelBuilder.Entity().HasData(Data.WorkoutPlanSeedData.Exercises()); } } public class CentralDbContextFactory : IDesignTimeDbContextFactory { public CentralDbContext CreateDbContext(string[] args) { var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false) .AddJsonFile($"appsettings.{env}.json", optional: true) .Build(); var optionsBuilder = new DbContextOptionsBuilder(); var connectionString = config.GetConnectionString("DefaultConnection"); optionsBuilder.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 0))); return new CentralDbContext(optionsBuilder.Options); } } }