Files
Core_Be/TenantDbContext.cs
HarithaRandunu 5d711bd13e Facility booking-mode redesign, multi-plan subscriptions, and Classes
Implements the facility-classes-booking-dashboard plan (backend):

- Facility.BookingMode (single Scheduled/AdHoc choice) replaced with
  independent SupportsClasses/SupportsBooking flags, so one facility
  can run a class and accept walk-in bookings; a running class now
  blocks walk-in booking for its window on both create and reschedule.
- CustomerPlan.GrantsGymAccess + CustomerPlanClass join table let a
  customer hold one gym-access plan plus any number of class-only
  plans. Replaced the old single-subscription resolution
  (GetActiveSubscriptionByCustomerId/HasAnyActiveSubscriptionAsync)
  with gym-access-aware and class-aware resolvers across booking
  creation/cancellation, plan assignment, and customer self-service.
  Fixed ChangeMembershipPlan/renewal plan-changes, which resolved "the
  subscription to replace" by most-recent-created and would have
  deactivated a member's class subscription instead of their gym one.
- New FacilityClass model: recurring class definitions (Daily/Weekly/
  Monthly/Custom) with lazy occurrence generation (8-week horizon,
  race-safe via a unique index) and proper unlimited-capacity handling.
  "Mark not held" replaces the old bare-cancel: cascades to cancel and
  refund every confirmed booking on the occurrence through the same
  core CancelBooking uses, and frees the slot for walk-in booking.
  Scheduling conflicts (a walk-in booking now overlapping a class) are
  surfaced for staff to resolve via the existing Reschedule flow.
- Renamed the pre-existing "AdHoc" booking-mode terminology to
  "WalkIn" throughout (routes, methods, DTOs, the stored BookingMode
  value) for a clearer, simpler name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-09 14:26:59 +05:30

408 lines
24 KiB
C#

using Fitness_core_BE.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Fitness_core_BE
{
// Per-tenant database: one gym's own operational data (staff, memberships, invoices,
// access control, attendance, ...). One instance of this context per resolved TenantId
// -- see Infra/Tenancy/TenantConnectionResolver.cs. Row-split entities (MealPlan/
// WorkoutPlan family, PaymentTransactions) hold this tenant's own client-authored plans
// / member payments here -- CentralDbContext holds the same entity types for admin
// templates / platform-billing rows.
public class TenantDbContext : DbContext
{
public TenantDbContext(DbContextOptions<TenantDbContext> options) : base(options) { }
// The local anchor every tenant-scoped Customer-referencing table FKs to instead of
// the central Customer.CusId directly (EF/MySQL can't enforce a FK across two
// separate connections).
public DbSet<TenantCustomerLink> TenantCustomerLink => Set<TenantCustomerLink>();
// Staff & RBAC
public DbSet<Staff> Staff => Set<Staff>();
public DbSet<StaffPayRate> StaffPayRates => Set<StaffPayRate>();
public DbSet<StaffPayment> StaffPayments => Set<StaffPayment>();
public DbSet<GymRole> GymRoles => Set<GymRole>();
public DbSet<GymRolePermission> GymRolePermissions => Set<GymRolePermission>();
// Memberships
public DbSet<CustomerPlan> CustomerPlans => Set<CustomerPlan>();
public DbSet<CustomerSubscription> CustomerSubscriptions => Set<CustomerSubscription>();
public DbSet<SubscriptionMember> SubscriptionMembers => Set<SubscriptionMember>();
public DbSet<SubscriptionInstallment> SubscriptionInstallments => Set<SubscriptionInstallment>();
public DbSet<CustomerSubItem> CustomerSubItems => Set<CustomerSubItem>();
public DbSet<CustomerInvoice> CustomerInvoices => Set<CustomerInvoice>();
public DbSet<CustomerInvoiceItem> CustomerInvoiceItems => Set<CustomerInvoiceItem>();
public DbSet<CustomerCards> CustomerCards => Set<CustomerCards>();
// Row-split: only rows with FK_CustomerInvoiceId set (member payments) live here --
// platform billing payments (FK_ClientInvoiceId set) live in CentralDbContext instead.
public DbSet<PaymentTransaction> PaymentTransactions => Set<PaymentTransaction>();
public DbSet<LedgerEntry> LedgerEntries => Set<LedgerEntry>();
public DbSet<ClientTerms> ClientTerms => Set<ClientTerms>();
public DbSet<CustomerTermsAcceptance> CustomerTermsAcceptances => Set<CustomerTermsAcceptance>();
// Access control & devices
public DbSet<AccessDevice> AccessDevices => Set<AccessDevice>();
public DbSet<AccessDeviceBinding> AccessDeviceBindings => Set<AccessDeviceBinding>();
public DbSet<AccessProfile> AccessProfiles => Set<AccessProfile>();
public DbSet<UnlinkedDeviceEnrollment> UnlinkedDeviceEnrollments => Set<UnlinkedDeviceEnrollment>();
public DbSet<CustomerAccess> CustomerAccess => Set<CustomerAccess>();
// Agent/PairingCode moved to CentralDbContext -- see its DbSet comment. FKAgentId on
// AccessDevice below stays a plain scalar column (unenforced cross-schema reference).
public DbSet<DeviceCommand> DeviceCommands => Set<DeviceCommand>();
public DbSet<Attendance> Attendances => Set<Attendance>();
// Client-authored (non-template) meal/workout plans + assignment join tables
public DbSet<MealPlan> MealPlans => Set<MealPlan>();
public DbSet<Meal> Meal => Set<Meal>();
public DbSet<CustomerMeal> CustomerMeals => Set<CustomerMeal>();
public DbSet<WorkoutPlan> WorkoutPlans => Set<WorkoutPlan>();
public DbSet<WorkoutItem> WorkoutItems => Set<WorkoutItem>();
public DbSet<WorkoutExercise> WorkoutExercises => Set<WorkoutExercise>();
public DbSet<CustomerWorkout> CustomerWorkouts => Set<CustomerWorkout>();
public DbSet<CustomerWorkoutExerciseCompletion> CustomerWorkoutExerciseCompletions => Set<CustomerWorkoutExerciseCompletion>();
// Multi-facility session booking + RFID time-bank cards (pool/court/class booking, one
// per club since single-tenant), and access-zone/device-scoping -- Facility merges what
// used to be two separate concepts (bookable capacity + access zone), see Models/Facility.cs.
public DbSet<Facility> Facilities => Set<Facility>();
public DbSet<FacilitySession> FacilitySessions => Set<FacilitySession>();
public DbSet<Booking> Bookings => Set<Booking>();
public DbSet<TimeBankCard> TimeBankCards => Set<TimeBankCard>();
public DbSet<TimeBankCardUsageLog> TimeBankCardUsageLogs => Set<TimeBankCardUsageLog>();
public DbSet<CustomerPlanFacility> CustomerPlanFacilities => Set<CustomerPlanFacility>();
// Recurring class definitions + their generated FacilitySession occurrences (see
// Models/FacilityClass.cs), and which CustomerPlan(s) are sold against a class.
public DbSet<FacilityClass> FacilityClasses => Set<FacilityClass>();
public DbSet<CustomerPlanClass> CustomerPlanClasses => Set<CustomerPlanClass>();
// Booking-scoped RFID tag inventory (assigned to a Booking at check-in, released at
// Complete/no-show sweep) -- see Models/RfidTag.cs. Not to be confused with
// TimeBankCard's own, unrelated RfidTagId string field.
public DbSet<RfidTag> RfidTags => Set<RfidTag>();
// Snapshot receipt created whenever a Booking's payment is recorded -- see
// Models/BookingInvoice.cs. Deliberately separate from CustomerInvoice (membership-only).
public DbSet<BookingInvoice> BookingInvoices => Set<BookingInvoice>();
// Simple inventory: Products, Suppliers, GRN (stock in), Sales (stock out), Returns
// (stock write-offs) -- see each model's own doc comment.
public DbSet<Product> Products => Set<Product>();
public DbSet<Supplier> Suppliers => Set<Supplier>();
public DbSet<GoodsReceivedNote> GoodsReceivedNotes => Set<GoodsReceivedNote>();
public DbSet<GoodsReceivedNoteItem> GoodsReceivedNoteItems => Set<GoodsReceivedNoteItem>();
public DbSet<Sale> Sales => Set<Sale>();
public DbSet<SaleItem> SaleItems => Set<SaleItem>();
public DbSet<StockReturn> StockReturns => Set<StockReturn>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Central-only entity types must never be part of this model at all. Ignoring
// individual navigations (below) isn't enough on its own: an unignored central
// collection nav elsewhere (e.g. Customer.CustomerSubscriptions/.CustomerMeals/
// .CustomerWorkouts, Client.ClientBusinesses) pulls the whole
// type into this context's model via EF's reachability-based discovery, which then
// invents a *second*, bogus shadow-FK relationship (e.g. a phantom
// "CustomerCusId" column) between that type and whatever tenant entity also
// happens to have its own real FK to it (e.g. CustomerSubscription, CustomerMeal,
// CustomerWorkout via TenantCustomerLink) -- breaking every query against that
// tenant entity with "Unknown column" once EF tries to select the phantom column.
// Blanket-ignoring the type at the root closes every such path at once, including
// ones not yet hit by a specific per-navigation Ignore below.
modelBuilder.Ignore<Customer>();
modelBuilder.Ignore<Client>();
// These were previously missing from this list. On separate physical databases
// (GYM's production layout) the gap was harmless -- the schema was hand-maintained/
// EnsureCreated, so these reachability-discovered phantom entities never actually
// got materialized as tables. Generating a real migration from this context's model
// (both contexts now share one DB) surfaced it immediately: each of these got its
// own duplicate table (usually singular-named, since they have no explicit DbSet
// here) alongside CentralDbContext's real, correctly-named one. Ignoring them here
// closes every reachability path at once, same as Customer/Client/WorkoutCategory
// above.
modelBuilder.Ignore<Admin>();
modelBuilder.Ignore<User>();
modelBuilder.Ignore<Ticket>();
modelBuilder.Ignore<TicketCategory>();
modelBuilder.Ignore<TicketMessage>();
modelBuilder.Ignore<UserDocument>();
modelBuilder.Ignore<DocumentTypes>();
modelBuilder.Ignore<ClientPackage>();
modelBuilder.Ignore<ClientCard>();
modelBuilder.Ignore<ClientBusiness>();
modelBuilder.Ignore<ClientSubscription>();
modelBuilder.Ignore<ClientInvoice>();
modelBuilder.Ignore<ClientInvoiceItem>();
modelBuilder.Ignore<NavItem>();
modelBuilder.Ignore<CusWeightLossProgress>();
modelBuilder.Ignore<CustomerTarget>();
// Plan categories are admin-authored and live centrally (CentralDbContext owns
// and seeds them; WorkoutRepository/MealRepository read and write them via
// _central). Left un-ignored, WorkoutPlan.WorkoutCategory / MealPlan.MealCategory
// drag the type into this model, which materialises an empty tenant-side
// "workoutcategory"/"mealcategory" table AND a real FK from the plan table to it
// -- so creating a gym's own plan fails the constraint against a table that is
// never populated.
modelBuilder.Ignore<WorkoutCategory>();
modelBuilder.Ignore<MealCategory>();
// Agent/PairingCode now live only in CentralDbContext (see its DbSet comment).
modelBuilder.Ignore<Agent>();
modelBuilder.Ignore<PairingCode>();
// Central-pointing navigation properties this context can't resolve -- the FK
// scalar column stays (it's still real data, just not a same-context relationship
// once central/tenant are physically separate DBs).
modelBuilder.Entity<Attendance>().Ignore(a => a.Client);
modelBuilder.Entity<Attendance>().Ignore(a => a.User);
modelBuilder.Entity<AccessProfile>().Ignore(p => p.Client);
modelBuilder.Entity<AccessProfile>().Ignore(p => p.User);
modelBuilder.Entity<AccessDevice>().Ignore(d => d.Client);
modelBuilder.Entity<CustomerAccess>().Ignore(c => c.Customer);
// A followed/assigned plan can be an admin-authored template (central) or this
// gym's own plan (tenant) -- GetWorkoutPlanListAsync serves customers the central
// templates. An enforced FK to the tenant plan table therefore can't hold: it
// rejects every follow of a template. The scalar id stays; which bucket it points
// at is resolved in the repository, the same way TenantCustomerLink ids are.
modelBuilder.Entity<CustomerWorkout>().Ignore(cw => cw.WorkoutPlan);
modelBuilder.Entity<CustomerMeal>().Ignore(cm => cm.MealPlan);
modelBuilder.Entity<DeviceCommand>().Ignore(c => c.Client);
modelBuilder.Entity<CustomerInvoice>().Ignore(i => i.Client);
modelBuilder.Entity<CustomerInvoice>().Ignore(i => i.User);
modelBuilder.Entity<CustomerPlan>().Ignore(p => p.Client);
modelBuilder.Entity<CustomerTermsAcceptance>().Ignore(t => t.Client);
modelBuilder.Entity<LedgerEntry>().Ignore(l => l.Client);
modelBuilder.Entity<GymRole>().Ignore(r => r.Client);
modelBuilder.Entity<Staff>().Ignore(s => s.User);
modelBuilder.Entity<Staff>().Ignore(s => s.Client);
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.ClientInvoice);
// Row-split entities: real DbSets in both contexts (see class comment), but on one
// shared database only one context's migration should physically create the table.
// CentralDbContext owns creation here (it also seeds admin-authored templates into
// MealPlan/WorkoutPlan* via HasData) -- ExcludeFromMigrations lets this context keep
// querying/writing these tables normally without generating a conflicting duplicate
// CreateTable for them.
modelBuilder.Entity<MealPlan>().ToTable(tb => tb.ExcludeFromMigrations());
modelBuilder.Entity<Meal>().ToTable(tb => tb.ExcludeFromMigrations());
modelBuilder.Entity<WorkoutPlan>().ToTable(tb => tb.ExcludeFromMigrations());
modelBuilder.Entity<WorkoutItem>().ToTable(tb => tb.ExcludeFromMigrations());
modelBuilder.Entity<WorkoutExercise>().ToTable(tb => tb.ExcludeFromMigrations());
modelBuilder.Entity<PaymentTransaction>().ToTable(tb => tb.ExcludeFromMigrations());
// Central-pointing scalar FKs on the new booking entities -- same pattern as every
// other .Client nav in this context (FKClientId column stays, real relationship
// doesn't, since Client lives only in CentralDbContext).
modelBuilder.Entity<Facility>().Ignore(f => f.Client);
modelBuilder.Entity<FacilitySession>().Ignore(s => s.Client);
modelBuilder.Entity<FacilityClass>().Ignore(fc => fc.Client);
modelBuilder.Entity<Booking>().Ignore(b => b.Client);
modelBuilder.Entity<BookingInvoice>().Ignore(i => i.Client);
modelBuilder.Entity<TimeBankCard>().Ignore(t => t.Client);
modelBuilder.Entity<TimeBankCardUsageLog>().Ignore(l => l.Client);
modelBuilder.Entity<Product>().Ignore(p => p.Client);
modelBuilder.Entity<Supplier>().Ignore(s => s.Client);
modelBuilder.Entity<GoodsReceivedNote>().Ignore(g => g.Client);
modelBuilder.Entity<Sale>().Ignore(s => s.Client);
modelBuilder.Entity<StockReturn>().Ignore(r => r.Client);
// A session's capacity is enforced by counting non-cancelled bookings against it at
// booking time, not by a unique index -- multiple guests/members can share one slot
// up to Facility.DefaultCapacity. What must stay unique is a member double-booking
// the same session twice. FKFacilitySessionId is null for every WalkIn booking, and
// MySQL/InnoDB never treats two NULLs as duplicates in a unique index, so this index
// is naturally a no-op for WalkIn bookings -- their own overlap-vs-NumberOfUnits check
// lives in FacilityRepository.GetOverlappingWalkInBookingCountAsync instead.
modelBuilder.Entity<Booking>()
.HasIndex(b => new { b.FKFacilitySessionId, b.FKCustomerId })
.IsUnique()
.HasFilter("`FKCustomerId` IS NOT NULL AND `Status` <> 'Cancelled'");
modelBuilder.Entity<FacilitySession>()
.HasIndex(s => new { s.FKFacilityId, s.StartTime });
modelBuilder.Entity<FacilityClass>()
.HasIndex(fc => fc.FKFacilityId);
// Guards against double-generating the same occurrence when two concurrent calendar
// loads both trigger EnsureOccurrencesGeneratedAsync for the same class -- a dedup
// check in code alone isn't race-safe. MySQL unique indexes ignore NULL, so every
// pre-existing, non-class session (FKFacilityClassId null) is unaffected.
modelBuilder.Entity<FacilitySession>()
.HasIndex(s => new { s.FKFacilityClassId, s.StartTime })
.IsUnique();
modelBuilder.Entity<TimeBankCard>()
.HasIndex(t => t.RfidTagId)
.IsUnique();
modelBuilder.Entity<Booking>()
.HasIndex(b => b.OtpCode);
// A customer can appear only once per subscription; installments are unique per position.
modelBuilder.Entity<SubscriptionMember>()
.HasIndex(sm => new { sm.FKSubscriptionId, sm.FKCustomerId })
.IsUnique();
modelBuilder.Entity<SubscriptionInstallment>()
.HasIndex(si => new { si.FKSubscriptionId, si.InstallmentNo })
.IsUnique();
// An exercise can only be marked completed once per customer workout.
modelBuilder.Entity<CustomerWorkoutExerciseCompletion>()
.HasIndex(c => new { c.FKCustomerWorkoutId, c.FKWorkoutExerciseId })
.IsUnique();
modelBuilder.Entity<AccessProfile>()
.HasIndex(p => new { p.FKClientId, p.FKUserId })
.IsUnique();
modelBuilder.Entity<AccessDeviceBinding>()
.HasIndex(b => new { b.FKAccessProfileId, b.FKDeviceId })
.IsUnique();
modelBuilder.Entity<AccessDeviceBinding>()
.HasIndex(b => new { b.FKDeviceId, b.DeviceEmployeeNo })
.IsUnique();
modelBuilder.Entity<AccessDeviceBinding>()
.HasOne(b => b.AccessProfile).WithMany(p => p.DeviceBindings)
.HasForeignKey(b => b.FKAccessProfileId).OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<AccessDeviceBinding>()
.HasOne(b => b.AccessDevice).WithMany()
.HasForeignKey(b => b.FKDeviceId).OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<AccessDevice>()
.HasIndex(d => new { d.FKClientId, d.HardwareId })
.IsUnique();
// Agent now lives centrally -- FKAgentId stays a real column, just not a
// same-context FK anymore (same pattern as every other central-pointing scalar).
modelBuilder.Entity<AccessDevice>().Ignore(d => d.Agent);
// One device per facility for now -- see AccessDevice.FKFacilityId comment. Lifting
// this to many-devices-per-facility later is just dropping this index.
modelBuilder.Entity<AccessDevice>()
.HasIndex(d => d.FKFacilityId)
.IsUnique()
.HasFilter("`FKFacilityId` IS NOT NULL");
modelBuilder.Entity<CustomerPlanFacility>()
.HasIndex(pf => new { pf.FKCustomerPlanId, pf.FKFacilityId })
.IsUnique();
modelBuilder.Entity<CustomerPlanFacility>()
.HasOne(pf => pf.CustomerPlan).WithMany()
.HasForeignKey(pf => pf.FKCustomerPlanId).OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<CustomerPlanFacility>()
.HasOne(pf => pf.Facility).WithMany()
.HasForeignKey(pf => pf.FKFacilityId).OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<CustomerPlanClass>()
.HasIndex(pc => new { pc.FKCustomerPlanId, pc.FKFacilityClassId })
.IsUnique();
modelBuilder.Entity<CustomerPlanClass>()
.HasOne(pc => pc.CustomerPlan).WithMany()
.HasForeignKey(pc => pc.FKCustomerPlanId).OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<CustomerPlanClass>()
.HasOne(pc => pc.FacilityClass).WithMany()
.HasForeignKey(pc => pc.FKFacilityClassId).OnDelete(DeleteBehavior.Restrict);
// RFID tag inventory is per-client; a tag code only needs to be unique within one
// club's pool.
modelBuilder.Entity<RfidTag>()
.HasIndex(t => new { t.FKClientId, t.TagCode })
.IsUnique();
// WalkIn bookings share a facility's NumberOfUnits, checked by counting overlapping
// non-cancelled/non-completed bookings at creation/reschedule time (see
// FacilityRepository.GetOverlappingWalkInBookingCountAsync) rather than a DB
// constraint -- an exclusion constraint over a time range isn't expressible as a
// simple unique index, and MySQL/Pomelo has no range-overlap index type. This index
// just makes that overlap query efficient.
modelBuilder.Entity<Booking>()
.HasIndex(b => new { b.FKFacilityId, b.StartTime, b.EndTime });
modelBuilder.Entity<BookingInvoice>()
.HasIndex(i => new { i.FKClientId, i.IssuedAt });
// Inventory: GRN/Sale items cascade with their header; Return/GRN/Sale restrict
// deleting a Product/Supplier that already has history (there's no delete endpoint
// for either, but Restrict keeps the data model honest either way).
modelBuilder.Entity<Product>()
.HasIndex(p => new { p.FKClientId, p.Name });
modelBuilder.Entity<Supplier>()
.HasIndex(s => new { s.FKClientId, s.Name });
modelBuilder.Entity<GoodsReceivedNote>()
.HasIndex(g => new { g.FKClientId, g.ReceivedAt });
modelBuilder.Entity<GoodsReceivedNote>()
.HasOne(g => g.Supplier).WithMany()
.HasForeignKey(g => g.FKSupplierId).OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<GoodsReceivedNoteItem>()
.HasOne(i => i.Grn).WithMany(g => g.Items)
.HasForeignKey(i => i.FKGrnId).OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<GoodsReceivedNoteItem>()
.HasOne(i => i.Product).WithMany()
.HasForeignKey(i => i.FKProductId).OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Sale>()
.HasIndex(s => new { s.FKClientId, s.SoldAt });
modelBuilder.Entity<SaleItem>()
.HasOne(i => i.Sale).WithMany(s => s.Items)
.HasForeignKey(i => i.FKSaleId).OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<SaleItem>()
.HasOne(i => i.Product).WithMany()
.HasForeignKey(i => i.FKProductId).OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<StockReturn>()
.HasIndex(r => new { r.FKClientId, r.ReturnedAt });
modelBuilder.Entity<StockReturn>()
.HasOne(r => r.Product).WithMany()
.HasForeignKey(r => r.FKProductId).OnDelete(DeleteBehavior.Restrict);
// Optional traceability links -- most returns have neither.
modelBuilder.Entity<StockReturn>()
.HasOne(r => r.Sale).WithMany()
.HasForeignKey(r => r.FKSaleId).OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<StockReturn>()
.HasOne(r => r.Grn).WithMany()
.HasForeignKey(r => r.FKGrnId).OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<UnlinkedDeviceEnrollment>()
.HasIndex(e => new { e.FKDeviceId, e.DeviceEmployeeNo, e.LinkedAt });
modelBuilder.Entity<Attendance>()
.HasIndex(a => a.SourceEventKey)
.IsUnique();
modelBuilder.Entity<DeviceCommand>()
.HasIndex(c => new { c.FKClientId, c.Status, c.NextAttemptAt });
modelBuilder.Entity<DeviceCommand>()
.Property(c => c.MaxAttempts)
.HasDefaultValue(5);
modelBuilder.Entity<GymRolePermission>()
.HasKey(p => new { p.FKGymRoleId, p.NavItemKey });
modelBuilder.Entity<GymRolePermission>().Ignore(p => p.NavItem);
}
}
public class TenantDbContextFactory : IDesignTimeDbContextFactory<TenantDbContext>
{
public TenantDbContext 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<TenantDbContext>();
var connectionString = config.GetConnectionString("DefaultConnection");
optionsBuilder.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 0)));
return new TenantDbContext(optionsBuilder.Options);
}
}
}