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.
225 lines
14 KiB
C#
225 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 = "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<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);
|
|
}
|
|
}
|
|
}
|