Files
Core_Be/CentralDbContext.cs
T
Dhananjaya99 bffc9b1686 first commit
2026-09-28 11:44:26 +05:30

224 lines
14 KiB
C#

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<CentralDbContext> options) : base(options) { }
// Identity & tenant registry
public DbSet<User> Users => Set<User>();
public DbSet<Admin> Admins => Set<Admin>();
public DbSet<Customer> Customer => Set<Customer>();
public DbSet<Client> Client => Set<Client>();
public DbSet<ClientBusiness> ClientBusiness => Set<ClientBusiness>();
// 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<Agent> Agents => Set<Agent>();
public DbSet<PairingCode> PairingCodes => Set<PairingCode>();
// 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<JobRunLog> JobRunLogs => Set<JobRunLog>();
// Customer-follows-you-everywhere data
public DbSet<CustomerHealth> CustomerHealth => Set<CustomerHealth>();
public DbSet<CustomerTarget> CustomerTargets => Set<CustomerTarget>();
public DbSet<CusWeightLossProgress> CusWeightLossProgress => Set<CusWeightLossProgress>();
public DbSet<CustomerNotification> CustomerNotifications => Set<CustomerNotification>();
// Admin-authored plan templates (IsTemplate=true / FKClientId=null rows only)
public DbSet<MealCategory> MealCategories => Set<MealCategory>();
public DbSet<MealPlan> MealPlans => Set<MealPlan>();
public DbSet<Meal> Meal => Set<Meal>();
public DbSet<WorkoutCategory> WorkoutCategories => Set<WorkoutCategory>();
public DbSet<WorkoutPlan> WorkoutPlans => Set<WorkoutPlan>();
public DbSet<WorkoutItem> WorkoutItems => Set<WorkoutItem>();
public DbSet<WorkoutExercise> WorkoutExercises => Set<WorkoutExercise>();
// Support / global catalog
public DbSet<Ticket> Tickets => Set<Ticket>();
public DbSet<TicketCategory> TicketCategories => Set<TicketCategory>();
public DbSet<TicketMessage> TicketMessages => Set<TicketMessage>();
public DbSet<NavItem> NavItems => Set<NavItem>();
public DbSet<UserDocument> UserDocuments => Set<UserDocument>();
public DbSet<DocumentTypes> DocumentTypes => Set<DocumentTypes>();
// Platform billing (HexaFitness -> gym)
public DbSet<ClientInvoice> ClientInvoices => Set<ClientInvoice>();
public DbSet<ClientCard> ClientCard => Set<ClientCard>();
public DbSet<ClientInvoiceItem> ClientInvoiceItems => Set<ClientInvoiceItem>();
public DbSet<ClientSubscription> ClientSubscriptions => Set<ClientSubscription>();
public DbSet<ClientSubItems> ClientSubItems => Set<ClientSubItems>();
public DbSet<ClientPackage> ClientPackages => Set<ClientPackage>();
public DbSet<ClientPayoutKyc> ClientPayoutKycs => Set<ClientPayoutKyc>();
public DbSet<ClientWallet> ClientWallets => Set<ClientWallet>();
public DbSet<WalletLedgerEntry> WalletLedgerEntries => Set<WalletLedgerEntry>();
public DbSet<PayoutBatch> PayoutBatches => Set<PayoutBatch>();
public DbSet<DevicePricingConfig> DevicePricingConfigs => Set<DevicePricingConfig>();
public DbSet<ClientDeviceBilling> ClientDeviceBillings => Set<ClientDeviceBilling>();
// Row-split: only rows with FK_ClientInvoiceId set (platform billing payments) live
// here -- member payments (FK_CustomerInvoiceId set) live in TenantDbContext instead.
public DbSet<PaymentTransaction> PaymentTransactions => Set<PaymentTransaction>();
// Ambiguous scope today (no FKClientId on the model) -- kept central pending the
// Phase-2 confirmation spike called out in the migration plan.
public DbSet<FeedPost> FeedPosts => Set<FeedPost>();
public DbSet<FeedLike> FeedLikes => Set<FeedLike>();
public DbSet<ActivityLog> ActivityLogs => Set<ActivityLog>();
public DbSet<Chat> Chat => Set<Chat>();
public DbSet<ChatMessages> ChatMessages => Set<ChatMessages>();
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<TenantCustomerLink>();
modelBuilder.Ignore<Staff>();
modelBuilder.Ignore<StaffPayRate>();
modelBuilder.Ignore<StaffPayment>();
modelBuilder.Ignore<GymRole>();
modelBuilder.Ignore<GymRolePermission>();
modelBuilder.Ignore<CustomerPlan>();
modelBuilder.Ignore<CustomerSubscription>();
modelBuilder.Ignore<SubscriptionMember>();
modelBuilder.Ignore<SubscriptionInstallment>();
modelBuilder.Ignore<CustomerSubItem>();
modelBuilder.Ignore<CustomerInvoice>();
modelBuilder.Ignore<CustomerInvoiceItem>();
modelBuilder.Ignore<CustomerCards>();
modelBuilder.Ignore<LedgerEntry>();
modelBuilder.Ignore<ClientTerms>();
modelBuilder.Ignore<CustomerTermsAcceptance>();
modelBuilder.Ignore<AccessDevice>();
modelBuilder.Ignore<AccessDeviceBinding>();
modelBuilder.Ignore<AccessProfile>();
modelBuilder.Ignore<UnlinkedDeviceEnrollment>();
modelBuilder.Ignore<CustomerAccess>();
modelBuilder.Ignore<DeviceCommand>();
modelBuilder.Ignore<Attendance>();
modelBuilder.Ignore<CustomerMeal>();
modelBuilder.Ignore<CustomerWorkout>();
modelBuilder.Ignore<CustomerWorkoutExerciseCompletion>();
modelBuilder.Entity<Agent>()
.HasIndex(a => a.ApiKeyHash)
.IsUnique();
modelBuilder.Entity<JobRunLog>()
.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<MealPlan>().Ignore(p => p.Client);
modelBuilder.Entity<WorkoutPlan>().Ignore(p => p.Client);
modelBuilder.Entity<PaymentTransaction>().Ignore(p => p.Client);
modelBuilder.Entity<PaymentTransaction>().Ignore(p => p.Customer);
modelBuilder.Entity<PaymentTransaction>().Ignore(p => p.CustomerCard);
modelBuilder.Entity<PaymentTransaction>().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<Customer>().Ignore(c => c.CustomerWorkouts);
modelBuilder.Entity<Customer>().Ignore(c => c.CustomerMeals);
modelBuilder.Entity<Customer>().Ignore(c => c.CustomerSubscriptions);
modelBuilder.Entity<NavItem>().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 = "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<DocumentTypes>().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<User>().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<WorkoutCategory>().HasData(Data.WorkoutPlanSeedData.Categories());
modelBuilder.Entity<WorkoutPlan>().HasData(Data.WorkoutPlanSeedData.Plans());
modelBuilder.Entity<WorkoutItem>().HasData(Data.WorkoutPlanSeedData.Items());
modelBuilder.Entity<WorkoutExercise>().HasData(Data.WorkoutPlanSeedData.Exercises());
}
}
public class CentralDbContextFactory : IDesignTimeDbContextFactory<CentralDbContext>
{
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<CentralDbContext>();
var connectionString = config.GetConnectionString("DefaultConnection");
optionsBuilder.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 0)));
return new CentralDbContext(optionsBuilder.Options);
}
}
}