2eb88b058e
- Implement Sale and SaleItem models for tracking sales transactions. - Create StockReturn model to manage stock adjustments for various reasons. - Introduce Supplier model to represent suppliers of products. - Develop InventoryRepository for CRUD operations on products, suppliers, sales, and stock returns. - Implement InventoryService to handle business logic for inventory management. - Update TenantDbContext to include new models and relationships. - Enhance ProfitLossRepository to account for sales, purchases, and stock returns in profit and loss calculations. - Modify Program.cs to register new inventory-related services.
381 lines
23 KiB
C#
381 lines
23 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>();
|
|
|
|
// 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<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 AdHoc booking, and
|
|
// MySQL/InnoDB never treats two NULLs as duplicates in a unique index, so this index
|
|
// is naturally a no-op for AdHoc bookings -- their own overlap-vs-NumberOfUnits check
|
|
// lives in BookingRepository.GetOverlappingAdhocBookingCountAsync 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<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);
|
|
|
|
// 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();
|
|
|
|
// AdHoc bookings share a facility's NumberOfUnits, checked by counting overlapping
|
|
// non-cancelled/non-completed bookings at creation/reschedule time (see
|
|
// BookingRepository.GetOverlappingAdhocBookingCountAsync) 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);
|
|
}
|
|
}
|
|
}
|