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:
@@ -13,5 +13,27 @@ namespace Fitness_core_BE.Interfaces
|
||||
Task<List<Booking>> GetBookingsForSession(Guid facilitySessionId);
|
||||
Task<List<Booking>> GetBookingsForCustomer(Guid customerId);
|
||||
Task<bool> CustomerHasActiveBookingForSession(Guid facilitySessionId, Guid customerId);
|
||||
|
||||
/// <summary>Unified roster query covering both Scheduled and AdHoc bookings -- backs
|
||||
/// GET api/client/bookings. All filters are optional/AND-combined.</summary>
|
||||
Task<List<Booking>> GetBookings(Guid clientId, Guid? facilityId, Guid? facilitySessionId, DateTime? fromUtc, DateTime? toUtc, string? status);
|
||||
|
||||
// --- Lazy overdue reconciliation (BookingService.SweepOverdueBookingsAsync), run at the
|
||||
// top of every check-in instead of a background job -- see Booking.Status doc comment. ---
|
||||
|
||||
/// <summary>Bulk-flips every Confirmed booking for this client whose EndTime has passed
|
||||
/// to NoShow. Returns the number of rows affected (informational/logging only).</summary>
|
||||
Task<int> MarkOverdueConfirmedAsNoShowAsync(Guid clientId, DateTime now);
|
||||
|
||||
/// <summary>Every CheckedIn booking for this client whose EndTime has passed and hasn't
|
||||
/// been checked out yet, with its RfidTag eagerly loaded so the caller can complete +
|
||||
/// release the tag on the same tracked graph in one SaveChanges.</summary>
|
||||
Task<List<Booking>> GetOverdueCheckedInBookingsAsync(Guid clientId, DateTime now);
|
||||
|
||||
/// <summary>Atomically flips a tag from Available to InUse -- the ONE place a real race
|
||||
/// matters (two staff picking the same tag for two different bookings must not both
|
||||
/// win), so this uses a conditional bulk update rather than read-then-write. Returns
|
||||
/// false if the tag was no longer Available.</summary>
|
||||
Task<bool> TryClaimRfidTagAsync(Guid tagId, DateTime now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,31 @@ namespace Fitness_core_BE.Interfaces
|
||||
Task<List<FacilitySession>> GetSessions(Guid clientId, Guid? facilityId, DateTime fromUtc, DateTime toUtc);
|
||||
Task<int> GetActiveBookingCount(Guid facilitySessionId);
|
||||
Task<FacilitySession?> CancelFacilitySession(Guid facilitySessionId, Guid clientId);
|
||||
|
||||
// Device binding (absorbed from the old IPlaceRepo -- one AccessDevice per Facility).
|
||||
Task<AccessDevice?> GetDeviceByIdForClientAsync(Guid deviceId, Guid clientId);
|
||||
Task<Dictionary<Guid, AccessDevice>> GetDevicesByFacilityIdsAsync(List<Guid> facilityIds);
|
||||
Task<bool> IsDeviceAssignedToAnotherFacilityAsync(Guid deviceId, Guid facilityId);
|
||||
Task AssignDeviceAsync(Guid facilityId, Guid deviceId);
|
||||
Task UnassignDeviceAsync(Guid facilityId);
|
||||
|
||||
// CustomerPlan <-> Facility access-scoping (many-to-many). Replace-all semantics, like a
|
||||
// tags field. Absorbed from the old IPlaceRepo.
|
||||
Task SetPlanFacilitiesAsync(Guid customerPlanId, List<Guid> facilityIds);
|
||||
Task<List<Guid>> GetFacilityIdsForPlanAsync(Guid customerPlanId);
|
||||
Task<Dictionary<Guid, List<Guid>>> GetFacilityIdsByPlanIdsAsync(List<Guid> customerPlanIds);
|
||||
Task<Dictionary<Guid, string>> GetFacilityNamesByIdsAsync(List<Guid> facilityIds);
|
||||
|
||||
// AdHoc booking support (new). See TenantDbContext's Booking (FKFacilityId, StartTime,
|
||||
// EndTime) index comment -- overlap is checked here rather than by a DB constraint.
|
||||
/// <summary>Counts non-cancelled/non-completed AdHoc bookings on this facility whose
|
||||
/// [StartTime, EndTime) overlaps the given window. Pass excludeBookingId when
|
||||
/// re-checking a booking being rescheduled, so it doesn't count against itself.</summary>
|
||||
Task<int> GetOverlappingAdhocBookingCountAsync(Guid facilityId, DateTime startTime, DateTime endTime, Guid? excludeBookingId = null);
|
||||
|
||||
/// <summary>Issues a row lock on the facility (SELECT ... FOR UPDATE) inside the caller's
|
||||
/// already-open unit-of-work transaction, serializing concurrent AdHoc-slot claims on the
|
||||
/// same facility so two overlapping bookings can't both pass the overlap-count check.</summary>
|
||||
Task LockFacilityForUpdateAsync(Guid facilityId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using Fitness_core_BE.Models;
|
||||
|
||||
namespace Fitness_core_BE.Interfaces
|
||||
{
|
||||
public interface IPlaceRepo
|
||||
{
|
||||
Task<List<Place>> GetAllPlaces(Guid clientId, bool includeInactive = false);
|
||||
Task<Place?> GetPlaceById(Guid placeId);
|
||||
Task<Place> CreatePlace(Place place);
|
||||
Task<Place> UpdatePlace(Place place);
|
||||
Task<Place?> SetPlaceActive(Guid placeId, Guid clientId, bool isActive);
|
||||
|
||||
Task<AccessDevice?> GetDeviceByIdForClientAsync(Guid deviceId, Guid clientId);
|
||||
Task<Dictionary<Guid, AccessDevice>> GetDevicesByPlaceIdsAsync(List<Guid> placeIds);
|
||||
Task<bool> IsDeviceAssignedToAnotherPlaceAsync(Guid deviceId, Guid placeId);
|
||||
Task AssignDeviceAsync(Guid placeId, Guid deviceId);
|
||||
Task UnassignDeviceAsync(Guid placeId);
|
||||
|
||||
// CustomerPlan <-> Place (many-to-many). Replace-all semantics, like a tags field.
|
||||
Task SetPlanPlacesAsync(Guid customerPlanId, List<Guid> placeIds);
|
||||
Task<List<Guid>> GetPlaceIdsForPlanAsync(Guid customerPlanId);
|
||||
Task<Dictionary<Guid, List<Guid>>> GetPlaceIdsByPlanIdsAsync(List<Guid> customerPlanIds);
|
||||
Task<Dictionary<Guid, string>> GetPlaceNamesByIdsAsync(List<Guid> placeIds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Fitness_core_BE.Models;
|
||||
|
||||
namespace Fitness_core_BE.Interfaces
|
||||
{
|
||||
public interface IRfidTagRepository
|
||||
{
|
||||
Task<List<RfidTag>> GetAllTags(Guid clientId, string? status = null);
|
||||
Task<RfidTag?> GetTagById(Guid tagId, Guid clientId);
|
||||
Task<bool> TagCodeExistsAsync(Guid clientId, string tagCode, Guid? excludeTagId = null);
|
||||
Task<RfidTag> CreateTag(RfidTag tag);
|
||||
Task<RfidTag> UpdateTag(RfidTag tag);
|
||||
|
||||
/// <summary>Only for staff-driven transitions (Available/Lost/Retired) -- rejects if the
|
||||
/// tag is currently InUse, since that transition only happens via booking check-in/
|
||||
/// complete/sweep.</summary>
|
||||
Task<RfidTag?> SetStatusAsync(Guid tagId, Guid clientId, string status);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,13 @@ namespace Fitness_core_BE.Interfaces.Services
|
||||
/// <summary>No membership at all -- pay-per-session, identity proof is the returned OTP.</summary>
|
||||
Task<object> CreateGuestBooking(object payload, HttpContext httpContext);
|
||||
|
||||
/// <summary>Member books an AdHoc-mode facility directly for an arbitrary time window, no
|
||||
/// pre-created session.</summary>
|
||||
Task<object> CreateMemberAdhocBooking(object payload, HttpContext httpContext);
|
||||
|
||||
/// <summary>Guest/staff walk-in books an AdHoc-mode facility directly.</summary>
|
||||
Task<object> CreateGuestAdhocBooking(object payload, HttpContext httpContext);
|
||||
|
||||
/// <summary>Renders the public, unauthenticated confirmation page a booking's ShareUrl
|
||||
/// points to -- the "copy the link and share it" receipt. Returns null if not found, so
|
||||
/// the controller can render a 404 page instead of a JSON error for a browser hit.</summary>
|
||||
@@ -18,11 +25,32 @@ namespace Fitness_core_BE.Interfaces.Services
|
||||
/// <summary>Staff records a guest booking's payment (cash/bank/at-venue) after the fact.</summary>
|
||||
Task<object> MarkGuestBookingPaid(object payload, HttpContext httpContext);
|
||||
|
||||
/// <summary>Staff/door check-in: verifies the OTP and marks the booking checked in.</summary>
|
||||
/// <summary>Staff/door check-in: verifies the OTP, optionally assigns an RFID tag, marks
|
||||
/// the booking checked in. Also lazily sweeps this client's overdue bookings first (see
|
||||
/// BookingService.SweepOverdueBookingsAsync) instead of a background job.</summary>
|
||||
Task<object> CheckInBooking(object payload, HttpContext httpContext);
|
||||
|
||||
Task<object> CancelBooking(object payload, HttpContext httpContext);
|
||||
|
||||
/// <summary>Staff hands a Confirmed guest booking to a different person (name/phone) --
|
||||
/// no cancel/recreate needed. Guest bookings only.</summary>
|
||||
Task<object> ReassignBooking(object payload, HttpContext httpContext);
|
||||
|
||||
/// <summary>Staff moves a Confirmed booking to a different time (AdHoc) or session
|
||||
/// (Scheduled), only onto a slot with room.</summary>
|
||||
Task<object> RescheduleBooking(object payload, HttpContext httpContext);
|
||||
|
||||
/// <summary>Staff manually flags a Confirmed, never-arrived booking.</summary>
|
||||
Task<object> MarkNoShow(object payload, HttpContext httpContext);
|
||||
|
||||
/// <summary>Staff manually ends a CheckedIn booking early (before the lazy sweep would
|
||||
/// catch it), releasing its RFID tag if any.</summary>
|
||||
Task<object> CompleteBooking(object payload, HttpContext httpContext);
|
||||
|
||||
Task<object> GetMyBookings(object payload, HttpContext httpContext);
|
||||
Task<object> GetSessionBookings(object payload, HttpContext httpContext);
|
||||
|
||||
/// <summary>Unified roster covering both booking modes.</summary>
|
||||
Task<object> GetBookings(object payload, HttpContext httpContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,10 @@ namespace Fitness_core_BE.Interfaces.Services
|
||||
Task<object> CreateFacilitySession(object payload, HttpContext httpContext);
|
||||
Task<object> GetSessions(object payload, HttpContext httpContext);
|
||||
Task<object> CancelFacilitySession(object payload, HttpContext httpContext);
|
||||
|
||||
// Absorbed from the old IPlaceService.
|
||||
Task<object> AssignDevice(object payload, HttpContext httpContext);
|
||||
|
||||
Task<object> GetAvailability(object payload, HttpContext httpContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Fitness_core_BE.Interfaces.Services
|
||||
{
|
||||
public interface IPlaceService
|
||||
{
|
||||
Task<object> GetAllPlaces(object payload, HttpContext httpContext);
|
||||
Task<object> CreatePlace(object payload, HttpContext httpContext);
|
||||
Task<object> UpdatePlace(object payload, HttpContext httpContext);
|
||||
Task<object> SetPlaceStatus(object payload, HttpContext httpContext);
|
||||
Task<object> AssignDevice(object payload, HttpContext httpContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Fitness_core_BE.Interfaces.Services
|
||||
{
|
||||
public interface IRfidTagService
|
||||
{
|
||||
Task<object> GetAllTags(object payload, HttpContext httpContext);
|
||||
Task<object> CreateTag(object payload, HttpContext httpContext);
|
||||
Task<object> UpdateTag(object payload, HttpContext httpContext);
|
||||
Task<object> SetTagStatus(object payload, HttpContext httpContext);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user