Refactor CustomerPlanService to use FacilityRepository, remove PlaceService, and introduce RfidTagService

- Updated CustomerPlanService to replace references to PlaceRepo with FacilityRepository for managing customer plans and associated facilities.
- Removed PlaceService and its related methods, consolidating functionality into FacilityService.
- Added RfidTagService for managing RFID tags, including methods for creating, updating, and setting statuses of tags.
- Updated TenantDbContext to include new DbSet for RfidTags and CustomerPlanFacilities, and adjusted model configurations accordingly.
- Enhanced FacilityService to support new booking modes and validation for facility creation and updates.
This commit is contained in:
2026-09-29 13:02:52 +05:30
parent bffc9b1686
commit 7d8a8478ff
39 changed files with 1596 additions and 678 deletions
+38 -18
View File
@@ -64,16 +64,19 @@ namespace Fitness_core_BE
public DbSet<CustomerWorkoutExerciseCompletion> CustomerWorkoutExerciseCompletions => Set<CustomerWorkoutExerciseCompletion>();
// Multi-facility session booking + RFID time-bank cards (pool/court/class booking, one
// per club since single-tenant -- see Models/Facility.cs and friends).
// 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>();
// Physical access zones (Pool Area, Indoor Gym, ...) -- see Models/Place.cs.
public DbSet<Place> Places => Set<Place>();
public DbSet<CustomerPlanPlace> CustomerPlanPlaces => Set<CustomerPlanPlace>();
// 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>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -183,12 +186,14 @@ namespace Fitness_core_BE
modelBuilder.Entity<Booking>().Ignore(b => b.Client);
modelBuilder.Entity<TimeBankCard>().Ignore(t => t.Client);
modelBuilder.Entity<TimeBankCardUsageLog>().Ignore(l => l.Client);
modelBuilder.Entity<Place>().Ignore(p => p.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.
// 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()
@@ -241,22 +246,37 @@ namespace Fitness_core_BE
// same-context FK anymore (same pattern as every other central-pointing scalar).
modelBuilder.Entity<AccessDevice>().Ignore(d => d.Agent);
// One device per place for now -- see AccessDevice.FKPlaceId comment. Lifting
// this to many-devices-per-place later is just dropping this index.
// 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.FKPlaceId)
.HasIndex(d => d.FKFacilityId)
.IsUnique()
.HasFilter("`FKPlaceId` IS NOT NULL");
.HasFilter("`FKFacilityId` IS NOT NULL");
modelBuilder.Entity<CustomerPlanPlace>()
.HasIndex(pp => new { pp.FKCustomerPlanId, pp.FKPlaceId })
modelBuilder.Entity<CustomerPlanFacility>()
.HasIndex(pf => new { pf.FKCustomerPlanId, pf.FKFacilityId })
.IsUnique();
modelBuilder.Entity<CustomerPlanPlace>()
.HasOne(pp => pp.CustomerPlan).WithMany()
.HasForeignKey(pp => pp.FKCustomerPlanId).OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<CustomerPlanPlace>()
.HasOne(pp => pp.Place).WithMany()
.HasForeignKey(pp => pp.FKPlaceId).OnDelete(DeleteBehavior.Restrict);
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<UnlinkedDeviceEnrollment>()
.HasIndex(e => new { e.FKDeviceId, e.DeviceEmployeeNo, e.LinkedAt });