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:
+409
-33
@@ -5,30 +5,28 @@ using Fitness_core_BE.Interfaces;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Fitness_core_BE.Services
|
||||
{
|
||||
public class BookingService : IBookingService
|
||||
public class BookingService(
|
||||
IBookingRepository bookingRepository,
|
||||
IFacilityRepository facilityRepository,
|
||||
IRfidTagRepository rfidTagRepository,
|
||||
ICustomerSubscriptionRepository customerSubscriptionRepository,
|
||||
IUnitOfWork uow,
|
||||
UserRoleHelper userRoleHelper) : IBookingService
|
||||
{
|
||||
private readonly IBookingRepository _bookingRepository;
|
||||
private readonly IFacilityRepository _facilityRepository;
|
||||
private readonly ICustomerSubscriptionRepository _customerSubscriptionRepository;
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly UserRoleHelper _userRoleHelper;
|
||||
private readonly IBookingRepository _bookingRepository = bookingRepository;
|
||||
private readonly IFacilityRepository _facilityRepository = facilityRepository;
|
||||
private readonly IRfidTagRepository _rfidTagRepository = rfidTagRepository;
|
||||
private readonly ICustomerSubscriptionRepository _customerSubscriptionRepository = customerSubscriptionRepository;
|
||||
private readonly IUnitOfWork _uow = uow;
|
||||
private readonly UserRoleHelper _userRoleHelper = userRoleHelper;
|
||||
|
||||
// ---------- Scheduled-mode creation (existing behavior, now also stamping the
|
||||
// denormalized FKFacilityId/StartTime/EndTime every Booking carries) ----------
|
||||
|
||||
public BookingService(
|
||||
IBookingRepository bookingRepository,
|
||||
IFacilityRepository facilityRepository,
|
||||
ICustomerSubscriptionRepository customerSubscriptionRepository,
|
||||
IUnitOfWork uow,
|
||||
UserRoleHelper userRoleHelper)
|
||||
{
|
||||
_bookingRepository = bookingRepository;
|
||||
_facilityRepository = facilityRepository;
|
||||
_customerSubscriptionRepository = customerSubscriptionRepository;
|
||||
_uow = uow;
|
||||
_userRoleHelper = userRoleHelper;
|
||||
}
|
||||
|
||||
public async Task<object> CreateMemberBooking(object payload, HttpContext httpContext)
|
||||
{
|
||||
@@ -78,6 +76,10 @@ namespace Fitness_core_BE.Services
|
||||
var booking = new Booking
|
||||
{
|
||||
FKFacilitySessionId = facilitySessionId,
|
||||
FKFacilityId = session.FKFacilityId,
|
||||
Facility = session.Facility,
|
||||
StartTime = session.StartTime,
|
||||
EndTime = session.EndTime,
|
||||
FKClientId = session.FKClientId,
|
||||
IsGuest = false,
|
||||
FKCustomerId = cusId,
|
||||
@@ -93,7 +95,7 @@ namespace Fitness_core_BE.Services
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return MapToDto(booking, session, includeOtp: false, BuildShareUrl(httpContext, booking.BookingId));
|
||||
return MapToDto(booking, includeOtp: false, BuildShareUrl(httpContext, booking.BookingId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -127,6 +129,10 @@ namespace Fitness_core_BE.Services
|
||||
var booking = new Booking
|
||||
{
|
||||
FKFacilitySessionId = facilitySessionId,
|
||||
FKFacilityId = session.FKFacilityId,
|
||||
Facility = session.Facility,
|
||||
StartTime = session.StartTime,
|
||||
EndTime = session.EndTime,
|
||||
FKClientId = session.FKClientId,
|
||||
IsGuest = true,
|
||||
GuestName = guestName,
|
||||
@@ -142,7 +148,7 @@ namespace Fitness_core_BE.Services
|
||||
await _uow.CommitAsync();
|
||||
|
||||
// The only time the OTP is ever returned -- this response IS the guest's receipt.
|
||||
return MapToDto(booking, session, includeOtp: true, BuildShareUrl(httpContext, booking.BookingId));
|
||||
return MapToDto(booking, includeOtp: true, BuildShareUrl(httpContext, booking.BookingId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -151,6 +157,123 @@ namespace Fitness_core_BE.Services
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- AdHoc-mode creation (new) ----------
|
||||
|
||||
public async Task<object> CreateMemberAdhocBooking(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var (facility, startTime, endTime, partySize) = await ValidateAdhocRequestAsync(payload);
|
||||
|
||||
var cusId = await _userRoleHelper.GetCusId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
|
||||
var booking = new Booking
|
||||
{
|
||||
FKFacilitySessionId = null,
|
||||
FKFacilityId = facility.FacilityId,
|
||||
Facility = facility,
|
||||
StartTime = startTime,
|
||||
EndTime = endTime,
|
||||
PartySize = partySize,
|
||||
FKClientId = facility.FKClientId,
|
||||
IsGuest = false,
|
||||
FKCustomerId = cusId,
|
||||
// Simplifying assumption mirroring the existing Scheduled member-booking
|
||||
// path: membership covers facility use, no separate per-booking charge.
|
||||
PaymentStatus = "NotRequired",
|
||||
Status = "Confirmed"
|
||||
};
|
||||
|
||||
await _bookingRepository.CreateBooking(booking);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return MapToDto(booking, includeOtp: false, BuildShareUrl(httpContext, booking.BookingId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> CreateGuestAdhocBooking(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var (facility, startTime, endTime, partySize) = await ValidateAdhocRequestAsync(payload);
|
||||
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
var guestName = data["guestName"].GetString() ?? throw new ArgumentException("guestName is required");
|
||||
var guestPhone = data["guestPhone"].GetString() ?? throw new ArgumentException("guestPhone is required");
|
||||
|
||||
var durationHours = (decimal)(endTime - startTime).TotalHours;
|
||||
|
||||
var booking = new Booking
|
||||
{
|
||||
FKFacilitySessionId = null,
|
||||
FKFacilityId = facility.FacilityId,
|
||||
Facility = facility,
|
||||
StartTime = startTime,
|
||||
EndTime = endTime,
|
||||
PartySize = partySize,
|
||||
FKClientId = facility.FKClientId,
|
||||
IsGuest = true,
|
||||
GuestName = guestName,
|
||||
GuestPhone = guestPhone,
|
||||
OtpCode = GenerateOtp(),
|
||||
AmountCharged = Math.Round(facility.PricePerHour * durationHours, 2),
|
||||
PaymentStatus = "Unpaid",
|
||||
Status = "Confirmed"
|
||||
};
|
||||
|
||||
await _bookingRepository.CreateBooking(booking);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return MapToDto(booking, includeOtp: true, BuildShareUrl(httpContext, booking.BookingId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shared validation for both AdHoc creation paths: facility must be AdHoc-mode,
|
||||
/// requested window must have a free unit. Locks the facility row for the duration of the
|
||||
/// caller's transaction so two concurrent claims on the same facility serialize instead of
|
||||
/// both reading "room available" before either commits.</summary>
|
||||
private async Task<(Facility facility, DateTime startTime, DateTime endTime, int? partySize)> ValidateAdhocRequestAsync(object payload)
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
var facilityId = Guid.Parse(data["facilityId"].GetString()!);
|
||||
var startTime = DateTime.Parse(data["startTime"].GetString()!).ToUniversalTime();
|
||||
var durationMinutes = data["durationMinutes"].GetInt32();
|
||||
if (durationMinutes < 1) throw new ArgumentException("durationMinutes must be at least 1");
|
||||
var endTime = startTime.AddMinutes(durationMinutes);
|
||||
|
||||
int? partySize = data.ContainsKey("partySize") && data["partySize"].ValueKind != JsonValueKind.Null
|
||||
? data["partySize"].GetInt32()
|
||||
: null;
|
||||
|
||||
var facility = await _facilityRepository.GetFacilityById(facilityId)
|
||||
?? throw new KeyNotFoundException("Facility not found");
|
||||
if (facility.BookingMode != "AdHoc")
|
||||
throw new ArgumentException("This facility does not support ad-hoc booking");
|
||||
if (!facility.IsActive)
|
||||
throw new ArgumentException("This facility is not active");
|
||||
|
||||
await _facilityRepository.LockFacilityForUpdateAsync(facilityId);
|
||||
var overlapping = await _facilityRepository.GetOverlappingAdhocBookingCountAsync(facilityId, startTime, endTime);
|
||||
if (overlapping >= facility.NumberOfUnits)
|
||||
throw new ArgumentException("No units are free for the requested time");
|
||||
|
||||
return (facility, startTime, endTime, partySize);
|
||||
}
|
||||
|
||||
// ---------- Payment / check-in / lifecycle ----------
|
||||
|
||||
public async Task<object> MarkGuestBookingPaid(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
@@ -169,7 +292,7 @@ namespace Fitness_core_BE.Services
|
||||
booking.AmountCharged = amountCharged;
|
||||
|
||||
await _uow.CommitAsync();
|
||||
return MapToDto(booking, booking.FacilitySession, includeOtp: false);
|
||||
return MapToDto(booking, includeOtp: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -185,19 +308,49 @@ namespace Fitness_core_BE.Services
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
var otpCode = data["otpCode"].GetString() ?? throw new ArgumentException("otpCode is required");
|
||||
Guid? rfidTagId = data.ContainsKey("rfidTagId") && data["rfidTagId"].ValueKind != JsonValueKind.Null
|
||||
? Guid.Parse(data["rfidTagId"].GetString()!)
|
||||
: null;
|
||||
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
|
||||
// Reconcile everything the passage of time has made stale for this client BEFORE
|
||||
// processing the new check-in, instead of a background job -- see
|
||||
// Booking.Status doc comment. If the booking being checked in is itself already
|
||||
// past its window, this sweep flips it to NoShow first, so the OTP lookup below
|
||||
// (Status == "Confirmed" only) correctly refuses it.
|
||||
await SweepOverdueBookingsAsync(clientId, DateTime.UtcNow);
|
||||
|
||||
var booking = await _bookingRepository.GetActiveBookingByOtp(otpCode)
|
||||
?? throw new KeyNotFoundException("No confirmed booking found for this code");
|
||||
if (booking.FKClientId != clientId)
|
||||
throw new KeyNotFoundException("No confirmed booking found for this code");
|
||||
|
||||
if (booking.OtpVerifiedAt.HasValue)
|
||||
throw new ArgumentException("This booking has already been checked in");
|
||||
|
||||
if (rfidTagId.HasValue)
|
||||
{
|
||||
var tag = await _rfidTagRepository.GetTagById(rfidTagId.Value, clientId)
|
||||
?? throw new KeyNotFoundException("RFID tag not found");
|
||||
if (tag.FKClientId != booking.FKClientId)
|
||||
throw new KeyNotFoundException("RFID tag not found");
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (!await _bookingRepository.TryClaimRfidTagAsync(rfidTagId.Value, now))
|
||||
throw new ArgumentException("This tag is no longer available -- pick another one.");
|
||||
|
||||
booking.FKRfidTagId = rfidTagId.Value;
|
||||
booking.RfidAssignedAt = now;
|
||||
booking.RfidTag = tag; // for the immediate response's RfidTagLabel only
|
||||
}
|
||||
|
||||
booking.OtpVerifiedAt = DateTime.UtcNow;
|
||||
booking.CheckedInAt = DateTime.UtcNow;
|
||||
booking.Status = "CheckedIn";
|
||||
|
||||
await _uow.CommitAsync();
|
||||
return MapToDto(booking, booking.FacilitySession, includeOtp: false);
|
||||
return MapToDto(booking, includeOtp: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -206,6 +359,31 @@ namespace Fitness_core_BE.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Lazily reconciles everything a background job would otherwise have swept:
|
||||
/// Confirmed bookings whose window passed with no check-in become NoShow; CheckedIn
|
||||
/// bookings whose window passed become Completed and release their tag. Runs at the top
|
||||
/// of every check-in, across every facility for this client (not scoped to whichever
|
||||
/// facility is being checked into right now), so the whole roster stays accurate as long
|
||||
/// as check-ins keep happening anywhere in the club.</summary>
|
||||
private async Task SweepOverdueBookingsAsync(Guid clientId, DateTime now)
|
||||
{
|
||||
await _bookingRepository.MarkOverdueConfirmedAsNoShowAsync(clientId, now);
|
||||
|
||||
var overdueCheckedIn = await _bookingRepository.GetOverdueCheckedInBookingsAsync(clientId, now);
|
||||
foreach (var booking in overdueCheckedIn)
|
||||
{
|
||||
booking.Status = "Completed";
|
||||
booking.CheckedOutAt = now;
|
||||
if (booking.RfidTag != null && booking.RfidReturnedAt == null)
|
||||
{
|
||||
booking.RfidTag.Status = "Available";
|
||||
booking.RfidTag.UpdatedAt = now;
|
||||
booking.RfidReturnedAt = now;
|
||||
}
|
||||
}
|
||||
// Caller's _uow.CommitAsync() persists these tracked changes alongside its own.
|
||||
}
|
||||
|
||||
public async Task<object> CancelBooking(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
@@ -216,8 +394,8 @@ namespace Fitness_core_BE.Services
|
||||
|
||||
var booking = await _bookingRepository.GetBookingById(bookingId)
|
||||
?? throw new KeyNotFoundException("Booking not found");
|
||||
if (booking.Status == "Cancelled")
|
||||
throw new ArgumentException("This booking is already cancelled");
|
||||
if (booking.Status is "Cancelled" or "NoShow" or "Completed")
|
||||
throw new ArgumentException($"This booking is already {booking.Status} and can't be cancelled");
|
||||
if (booking.Status == "CheckedIn")
|
||||
throw new ArgumentException("This booking has already been checked in and can't be cancelled");
|
||||
|
||||
@@ -235,7 +413,7 @@ namespace Fitness_core_BE.Services
|
||||
}
|
||||
|
||||
await _uow.CommitAsync();
|
||||
return MapToDto(booking, booking.FacilitySession, includeOtp: false);
|
||||
return MapToDto(booking, includeOtp: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -244,11 +422,180 @@ namespace Fitness_core_BE.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> ReassignBooking(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
var bookingId = Guid.Parse(data["bookingId"].GetString()!);
|
||||
|
||||
var booking = await _bookingRepository.GetBookingById(bookingId)
|
||||
?? throw new KeyNotFoundException("Booking not found");
|
||||
if (!booking.IsGuest)
|
||||
throw new ArgumentException("Only guest bookings can be reassigned");
|
||||
if (booking.Status != "Confirmed")
|
||||
throw new ArgumentException("Only a booking that hasn't been checked in yet can be reassigned");
|
||||
|
||||
if (data.ContainsKey("guestName")) booking.GuestName = data["guestName"].GetString() ?? booking.GuestName;
|
||||
if (data.ContainsKey("guestPhone")) booking.GuestPhone = data["guestPhone"].GetString() ?? booking.GuestPhone;
|
||||
|
||||
await _uow.CommitAsync();
|
||||
return MapToDto(booking, includeOtp: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> RescheduleBooking(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
var bookingId = Guid.Parse(data["bookingId"].GetString()!);
|
||||
|
||||
var booking = await _bookingRepository.GetBookingById(bookingId)
|
||||
?? throw new KeyNotFoundException("Booking not found");
|
||||
if (booking.Status != "Confirmed")
|
||||
throw new ArgumentException("Only a booking that hasn't been checked in yet can be rescheduled");
|
||||
|
||||
if (booking.FKFacilitySessionId == null)
|
||||
{
|
||||
// AdHoc: move to a new time on the same facility.
|
||||
if (!data.ContainsKey("startTime") || !data.ContainsKey("durationMinutes"))
|
||||
throw new ArgumentException("startTime and durationMinutes are required to reschedule an ad-hoc booking");
|
||||
|
||||
var startTime = DateTime.Parse(data["startTime"].GetString()!).ToUniversalTime();
|
||||
var durationMinutes = data["durationMinutes"].GetInt32();
|
||||
if (durationMinutes < 1) throw new ArgumentException("durationMinutes must be at least 1");
|
||||
var endTime = startTime.AddMinutes(durationMinutes);
|
||||
|
||||
await _facilityRepository.LockFacilityForUpdateAsync(booking.FKFacilityId);
|
||||
var overlapping = await _facilityRepository.GetOverlappingAdhocBookingCountAsync(
|
||||
booking.FKFacilityId, startTime, endTime, excludeBookingId: booking.BookingId);
|
||||
|
||||
var facility = booking.Facility ?? await _facilityRepository.GetFacilityById(booking.FKFacilityId)
|
||||
?? throw new KeyNotFoundException("Facility not found");
|
||||
if (overlapping >= facility.NumberOfUnits)
|
||||
throw new ArgumentException("No units are free for the requested time");
|
||||
|
||||
if (booking.PaymentStatus != "Paid" && booking.IsGuest)
|
||||
{
|
||||
var durationHours = (decimal)(endTime - startTime).TotalHours;
|
||||
booking.AmountCharged = Math.Round(facility.PricePerHour * durationHours, 2);
|
||||
}
|
||||
|
||||
booking.StartTime = startTime;
|
||||
booking.EndTime = endTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scheduled: move to a different pre-created session on the same facility.
|
||||
if (!data.ContainsKey("facilitySessionId"))
|
||||
throw new ArgumentException("facilitySessionId is required to reschedule a scheduled booking");
|
||||
|
||||
var newSessionId = Guid.Parse(data["facilitySessionId"].GetString()!);
|
||||
var newSession = await _facilityRepository.GetFacilitySessionById(newSessionId)
|
||||
?? throw new KeyNotFoundException("Facility session not found");
|
||||
if (newSession.FKFacilityId != booking.FKFacilityId)
|
||||
throw new ArgumentException("The new session must be on the same facility");
|
||||
if (newSession.IsCancelled)
|
||||
throw new ArgumentException("This session has been cancelled");
|
||||
if (newSession.StartTime <= DateTime.UtcNow)
|
||||
throw new ArgumentException("This session has already started");
|
||||
|
||||
var capacity = newSession.Capacity ?? newSession.Facility?.DefaultCapacity ?? 1;
|
||||
var bookedCount = await _facilityRepository.GetActiveBookingCount(newSessionId);
|
||||
// Excluding this booking's own current session membership naturally falls out
|
||||
// here since bookedCount only counts rows already on newSessionId, which this
|
||||
// booking isn't yet.
|
||||
if (bookedCount >= capacity)
|
||||
throw new ArgumentException("This session is fully booked");
|
||||
|
||||
booking.FKFacilitySessionId = newSessionId;
|
||||
booking.StartTime = newSession.StartTime;
|
||||
booking.EndTime = newSession.EndTime;
|
||||
}
|
||||
|
||||
await _uow.CommitAsync();
|
||||
return MapToDto(booking, includeOtp: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> MarkNoShow(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
var bookingId = Guid.Parse(data["bookingId"].GetString()!);
|
||||
|
||||
var booking = await _bookingRepository.GetBookingById(bookingId)
|
||||
?? throw new KeyNotFoundException("Booking not found");
|
||||
if (booking.Status != "Confirmed")
|
||||
throw new ArgumentException("Only a Confirmed booking can be marked no-show");
|
||||
|
||||
booking.Status = "NoShow";
|
||||
|
||||
await _uow.CommitAsync();
|
||||
return MapToDto(booking, includeOtp: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> CompleteBooking(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
var bookingId = Guid.Parse(data["bookingId"].GetString()!);
|
||||
|
||||
var booking = await _bookingRepository.GetBookingById(bookingId)
|
||||
?? throw new KeyNotFoundException("Booking not found");
|
||||
if (booking.Status != "CheckedIn")
|
||||
throw new ArgumentException("Only a CheckedIn booking can be completed");
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
booking.Status = "Completed";
|
||||
booking.CheckedOutAt = now;
|
||||
if (booking.RfidTag != null && booking.RfidReturnedAt == null)
|
||||
{
|
||||
booking.RfidTag.Status = "Available";
|
||||
booking.RfidTag.UpdatedAt = now;
|
||||
booking.RfidReturnedAt = now;
|
||||
}
|
||||
|
||||
await _uow.CommitAsync();
|
||||
return MapToDto(booking, includeOtp: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Reads ----------
|
||||
|
||||
public async Task<object> GetMyBookings(object payload, HttpContext httpContext)
|
||||
{
|
||||
var cusId = await _userRoleHelper.GetCusId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
var bookings = await _bookingRepository.GetBookingsForCustomer(cusId);
|
||||
return bookings.Select(b => MapToDto(b, b.FacilitySession, includeOtp: false)).ToList();
|
||||
return bookings.Select(b => MapToDto(b, includeOtp: false)).ToList();
|
||||
}
|
||||
|
||||
public async Task<object> GetSessionBookings(object payload, HttpContext httpContext)
|
||||
@@ -259,7 +606,28 @@ namespace Fitness_core_BE.Services
|
||||
var bookings = await _bookingRepository.GetBookingsForSession(facilitySessionId);
|
||||
// Staff view -- never expose guest OTPs here either; the door check-in flow reads
|
||||
// the OTP the guest already has, it doesn't need staff to look it up beforehand.
|
||||
return bookings.Select(b => MapToDto(b, b.FacilitySession, includeOtp: false)).ToList();
|
||||
return bookings.Select(b => MapToDto(b, includeOtp: false)).ToList();
|
||||
}
|
||||
|
||||
public async Task<object> GetBookings(object payload, HttpContext httpContext)
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
|
||||
Guid? facilityId = data.ContainsKey("facilityId") && data["facilityId"].ValueKind != JsonValueKind.Null
|
||||
? Guid.Parse(data["facilityId"].GetString()!) : null;
|
||||
Guid? facilitySessionId = data.ContainsKey("facilitySessionId") && data["facilitySessionId"].ValueKind != JsonValueKind.Null
|
||||
? Guid.Parse(data["facilitySessionId"].GetString()!) : null;
|
||||
DateTime? fromUtc = data.ContainsKey("fromDate") && data["fromDate"].ValueKind != JsonValueKind.Null
|
||||
? DateTime.Parse(data["fromDate"].GetString()!).ToUniversalTime() : null;
|
||||
DateTime? toUtc = data.ContainsKey("toDate") && data["toDate"].ValueKind != JsonValueKind.Null
|
||||
? DateTime.Parse(data["toDate"].GetString()!).ToUniversalTime() : null;
|
||||
string? status = data.ContainsKey("status") && data["status"].ValueKind != JsonValueKind.Null
|
||||
? data["status"].GetString() : null;
|
||||
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
var bookings = await _bookingRepository.GetBookings(clientId, facilityId, facilitySessionId, fromUtc, toUtc, status);
|
||||
|
||||
return bookings.Select(b => MapToDto(b, includeOtp: false)).ToList();
|
||||
}
|
||||
|
||||
public async Task<object?> GetBookingView(object payload)
|
||||
@@ -273,7 +641,7 @@ namespace Fitness_core_BE.Services
|
||||
|
||||
// The share link itself is the credential (an unguessable GUID) -- the OTP is only
|
||||
// withheld once it's already been used, same rule as every other read path.
|
||||
return MapToDto(booking, booking.FacilitySession, includeOtp: !booking.OtpVerifiedAt.HasValue);
|
||||
return MapToDto(booking, includeOtp: !booking.OtpVerifiedAt.HasValue);
|
||||
}
|
||||
|
||||
private static string GenerateOtp() => RandomNumberGenerator.GetInt32(100000, 1000000).ToString();
|
||||
@@ -284,24 +652,32 @@ namespace Fitness_core_BE.Services
|
||||
private static string BuildShareUrl(HttpContext httpContext, Guid bookingId)
|
||||
=> $"{httpContext.Request.Scheme}://{httpContext.Request.Host}/api/public/bookings/{bookingId}/view";
|
||||
|
||||
private static BookingDto MapToDto(Booking booking, FacilitySession? session, bool includeOtp, string? shareUrl = null) => new()
|
||||
private static BookingDto MapToDto(Booking booking, bool includeOtp, string? shareUrl = null) => new()
|
||||
{
|
||||
BookingId = booking.BookingId,
|
||||
FacilitySessionId = booking.FKFacilitySessionId,
|
||||
FacilityName = session?.Facility?.Name,
|
||||
SessionStartTime = session?.StartTime,
|
||||
SessionEndTime = session?.EndTime,
|
||||
FacilityId = booking.FKFacilityId,
|
||||
FacilityName = booking.Facility?.Name,
|
||||
BookingMode = booking.Facility?.BookingMode ?? (booking.FKFacilitySessionId.HasValue ? "Scheduled" : "AdHoc"),
|
||||
StartTime = booking.StartTime,
|
||||
EndTime = booking.EndTime,
|
||||
PartySize = booking.PartySize,
|
||||
IsGuest = booking.IsGuest,
|
||||
CustomerId = booking.FKCustomerId,
|
||||
GuestName = booking.GuestName,
|
||||
GuestPhone = booking.GuestPhone,
|
||||
OtpCode = includeOtp ? booking.OtpCode : null,
|
||||
OtpVerified = booking.OtpVerifiedAt.HasValue,
|
||||
RfidTagId = booking.FKRfidTagId,
|
||||
RfidTagLabel = booking.RfidTag?.Label ?? booking.RfidTag?.TagCode,
|
||||
RfidAssignedAt = booking.RfidAssignedAt,
|
||||
RfidReturnedAt = booking.RfidReturnedAt,
|
||||
PaymentStatus = booking.PaymentStatus,
|
||||
AmountCharged = booking.AmountCharged,
|
||||
Status = booking.Status,
|
||||
BookedAt = booking.BookedAt,
|
||||
CheckedInAt = booking.CheckedInAt,
|
||||
CheckedOutAt = booking.CheckedOutAt,
|
||||
ShareUrl = shareUrl
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ using Fitness_core_BE.Helpers;
|
||||
|
||||
namespace Fitness_core_BE.Services.CustomerPlanService
|
||||
{
|
||||
public class CustomerPlanService : ICustomerPlanService
|
||||
public class CustomerPlanService(
|
||||
ICustomerPlanRepository customerPlanRepository,
|
||||
IFacilityRepository facilityRepository,
|
||||
IUnitOfWork uow,
|
||||
UserRoleHelper userRoleHelper) : ICustomerPlanService
|
||||
{
|
||||
private const string ScopeIndividual = "Individual";
|
||||
private const string ScopeGroup = "Group";
|
||||
@@ -20,22 +24,10 @@ namespace Fitness_core_BE.Services.CustomerPlanService
|
||||
private const string BillingPrepaid = "Prepaid";
|
||||
private const string BillingPostpaid = "Postpaid";
|
||||
|
||||
private readonly ICustomerPlanRepository _customerPlanRepository;
|
||||
private readonly IPlaceRepo _placeRepo;
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly UserRoleHelper _userRoleHelper;
|
||||
|
||||
public CustomerPlanService(
|
||||
ICustomerPlanRepository customerPlanRepository,
|
||||
IPlaceRepo placeRepo,
|
||||
IUnitOfWork uow,
|
||||
UserRoleHelper userRoleHelper)
|
||||
{
|
||||
_customerPlanRepository = customerPlanRepository;
|
||||
_placeRepo = placeRepo;
|
||||
_uow = uow;
|
||||
_userRoleHelper = userRoleHelper;
|
||||
}
|
||||
private readonly ICustomerPlanRepository _customerPlanRepository = customerPlanRepository;
|
||||
private readonly IFacilityRepository _facilityRepository = facilityRepository;
|
||||
private readonly IUnitOfWork _uow = uow;
|
||||
private readonly UserRoleHelper _userRoleHelper = userRoleHelper;
|
||||
|
||||
public async Task<object> GetAllCustomerPlans(object payload, HttpContext httpContext)
|
||||
{
|
||||
@@ -58,9 +50,9 @@ namespace Fitness_core_BE.Services.CustomerPlanService
|
||||
|
||||
var plans = await _customerPlanRepository.GetAllCustomerPlans(includeInactive, clientId);
|
||||
|
||||
var placeIdsByPlan = await _placeRepo.GetPlaceIdsByPlanIdsAsync(plans.Select(p => p.CustomerPlanId).ToList());
|
||||
var placeIdsByPlan = await _facilityRepository.GetFacilityIdsByPlanIdsAsync(plans.Select(p => p.CustomerPlanId).ToList());
|
||||
var allPlaceIds = placeIdsByPlan.Values.SelectMany(ids => ids).Distinct().ToList();
|
||||
var placeNames = await _placeRepo.GetPlaceNamesByIdsAsync(allPlaceIds);
|
||||
var placeNames = await _facilityRepository.GetFacilityNamesByIdsAsync(allPlaceIds);
|
||||
|
||||
return plans.Select(p =>
|
||||
{
|
||||
@@ -142,7 +134,7 @@ namespace Fitness_core_BE.Services.CustomerPlanService
|
||||
|
||||
var placeIds = DataExtractor.GetOptionalGuidList(data, "placeIds");
|
||||
if (placeIds != null)
|
||||
await _placeRepo.SetPlanPlacesAsync(result.CustomerPlanId, placeIds);
|
||||
await _facilityRepository.SetPlanFacilitiesAsync(result.CustomerPlanId, placeIds);
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
@@ -206,7 +198,7 @@ namespace Fitness_core_BE.Services.CustomerPlanService
|
||||
|
||||
var placeIds = DataExtractor.GetOptionalGuidList(data, "placeIds");
|
||||
if (placeIds != null)
|
||||
await _placeRepo.SetPlanPlacesAsync(result.CustomerPlanId, placeIds);
|
||||
await _facilityRepository.SetPlanFacilitiesAsync(result.CustomerPlanId, placeIds);
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
@@ -400,8 +392,8 @@ namespace Fitness_core_BE.Services.CustomerPlanService
|
||||
|
||||
private async Task<CustomerPlanDto> MapToDtoWithPlacesAsync(Models.CustomerPlan plan, string? clientName)
|
||||
{
|
||||
var placeIds = await _placeRepo.GetPlaceIdsForPlanAsync(plan.CustomerPlanId);
|
||||
var placeNames = await _placeRepo.GetPlaceNamesByIdsAsync(placeIds);
|
||||
var placeIds = await _facilityRepository.GetFacilityIdsForPlanAsync(plan.CustomerPlanId);
|
||||
var placeNames = await _facilityRepository.GetFacilityNamesByIdsAsync(placeIds);
|
||||
return MapToDto(plan, clientName, placeIds, placeNames);
|
||||
}
|
||||
|
||||
|
||||
+162
-22
@@ -10,7 +10,7 @@ namespace Fitness_core_BE.Services
|
||||
{
|
||||
public class FacilityService : IFacilityService
|
||||
{
|
||||
private static readonly string[] ValidFacilityTypes = { "Gym", "Pool", "Court", "Class" };
|
||||
private static readonly string[] ValidBookingModes = ["Scheduled", "AdHoc"];
|
||||
|
||||
private readonly IFacilityRepository _facilityRepository;
|
||||
private readonly IUnitOfWork _uow;
|
||||
@@ -31,7 +31,9 @@ namespace Fitness_core_BE.Services
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
var facilities = await _facilityRepository.GetAllFacilities(clientId, includeInactive);
|
||||
|
||||
return facilities.Select(MapToDto).ToList();
|
||||
var devicesByFacility = await _facilityRepository.GetDevicesByFacilityIdsAsync(facilities.Select(f => f.FacilityId).ToList());
|
||||
|
||||
return facilities.Select(f => MapToDto(f, devicesByFacility.GetValueOrDefault(f.FacilityId))).ToList();
|
||||
}
|
||||
|
||||
public async Task<object> CreateFacility(object payload, HttpContext httpContext)
|
||||
@@ -41,11 +43,22 @@ namespace Fitness_core_BE.Services
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
if (!data.ContainsKey("name")) throw new KeyNotFoundException("name is required");
|
||||
if (!data.ContainsKey("facilityType")) throw new KeyNotFoundException("facilityType is required");
|
||||
|
||||
var facilityType = data["facilityType"].GetString() ?? "";
|
||||
if (!ValidFacilityTypes.Contains(facilityType))
|
||||
throw new ArgumentException($"facilityType must be one of: {string.Join(", ", ValidFacilityTypes)}");
|
||||
var bookingMode = data.ContainsKey("bookingMode") ? data["bookingMode"].GetString() ?? "Scheduled" : "Scheduled";
|
||||
if (!ValidBookingModes.Contains(bookingMode))
|
||||
throw new ArgumentException($"bookingMode must be one of: {string.Join(", ", ValidBookingModes)}");
|
||||
|
||||
var defaultCapacity = data.ContainsKey("defaultCapacity") ? data["defaultCapacity"].GetInt32() : 1;
|
||||
if (bookingMode == "Scheduled" && defaultCapacity < 1)
|
||||
throw new ArgumentException("defaultCapacity must be at least 1");
|
||||
|
||||
var numberOfUnits = data.ContainsKey("numberOfUnits") ? data["numberOfUnits"].GetInt32() : 1;
|
||||
if (bookingMode == "AdHoc" && numberOfUnits < 1)
|
||||
throw new ArgumentException("numberOfUnits must be at least 1");
|
||||
|
||||
var pricePerHour = data.ContainsKey("pricePerHour") ? data["pricePerHour"].GetDecimal() : 0m;
|
||||
if (pricePerHour < 0)
|
||||
throw new ArgumentException("pricePerHour cannot be negative");
|
||||
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
|
||||
@@ -53,18 +66,17 @@ namespace Fitness_core_BE.Services
|
||||
{
|
||||
FKClientId = clientId,
|
||||
Name = data["name"].GetString() ?? "",
|
||||
FacilityType = facilityType,
|
||||
BookingMode = bookingMode,
|
||||
Description = data.ContainsKey("description") ? data["description"].GetString() : null,
|
||||
DefaultCapacity = data.ContainsKey("defaultCapacity") ? data["defaultCapacity"].GetInt32() : 1
|
||||
DefaultCapacity = defaultCapacity,
|
||||
NumberOfUnits = numberOfUnits,
|
||||
PricePerHour = pricePerHour
|
||||
};
|
||||
|
||||
if (facility.DefaultCapacity < 1)
|
||||
throw new ArgumentException("defaultCapacity must be at least 1");
|
||||
|
||||
var result = await _facilityRepository.CreateFacility(facility);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return MapToDto(result);
|
||||
return MapToDto(result, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -86,12 +98,12 @@ namespace Fitness_core_BE.Services
|
||||
?? throw new KeyNotFoundException("Facility not found");
|
||||
|
||||
if (data.ContainsKey("name")) existing.Name = data["name"].GetString() ?? existing.Name;
|
||||
if (data.ContainsKey("facilityType"))
|
||||
if (data.ContainsKey("bookingMode"))
|
||||
{
|
||||
var facilityType = data["facilityType"].GetString() ?? existing.FacilityType;
|
||||
if (!ValidFacilityTypes.Contains(facilityType))
|
||||
throw new ArgumentException($"facilityType must be one of: {string.Join(", ", ValidFacilityTypes)}");
|
||||
existing.FacilityType = facilityType;
|
||||
var bookingMode = data["bookingMode"].GetString() ?? existing.BookingMode;
|
||||
if (!ValidBookingModes.Contains(bookingMode))
|
||||
throw new ArgumentException($"bookingMode must be one of: {string.Join(", ", ValidBookingModes)}");
|
||||
existing.BookingMode = bookingMode;
|
||||
}
|
||||
if (data.ContainsKey("description")) existing.Description = data["description"].GetString();
|
||||
if (data.ContainsKey("defaultCapacity"))
|
||||
@@ -100,12 +112,25 @@ namespace Fitness_core_BE.Services
|
||||
if (capacity < 1) throw new ArgumentException("defaultCapacity must be at least 1");
|
||||
existing.DefaultCapacity = capacity;
|
||||
}
|
||||
if (data.ContainsKey("numberOfUnits"))
|
||||
{
|
||||
var units = data["numberOfUnits"].GetInt32();
|
||||
if (units < 1) throw new ArgumentException("numberOfUnits must be at least 1");
|
||||
existing.NumberOfUnits = units;
|
||||
}
|
||||
if (data.ContainsKey("pricePerHour"))
|
||||
{
|
||||
var price = data["pricePerHour"].GetDecimal();
|
||||
if (price < 0) throw new ArgumentException("pricePerHour cannot be negative");
|
||||
existing.PricePerHour = price;
|
||||
}
|
||||
if (data.ContainsKey("isActive")) existing.IsActive = data["isActive"].GetBoolean();
|
||||
|
||||
var result = await _facilityRepository.UpdateFacility(existing);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return MapToDto(result);
|
||||
var devicesByFacility = await _facilityRepository.GetDevicesByFacilityIdsAsync(new List<Guid> { result.FacilityId });
|
||||
return MapToDto(result, devicesByFacility.GetValueOrDefault(result.FacilityId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -128,7 +153,7 @@ namespace Fitness_core_BE.Services
|
||||
?? throw new KeyNotFoundException("Facility not found");
|
||||
|
||||
await _uow.CommitAsync();
|
||||
return MapToDto(facility);
|
||||
return MapToDto(facility, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -154,6 +179,9 @@ namespace Fitness_core_BE.Services
|
||||
if (facility == null || facility.FKClientId != clientId)
|
||||
throw new KeyNotFoundException("Facility not found");
|
||||
|
||||
if (facility.BookingMode != "Scheduled")
|
||||
throw new ArgumentException("Sessions can only be created for Scheduled-mode facilities");
|
||||
|
||||
var startTime = DateTime.Parse(data["startTime"].GetString()!).ToUniversalTime();
|
||||
var endTime = DateTime.Parse(data["endTime"].GetString()!).ToUniversalTime();
|
||||
if (endTime <= startTime)
|
||||
@@ -236,15 +264,127 @@ namespace Fitness_core_BE.Services
|
||||
}
|
||||
}
|
||||
|
||||
private static FacilityDto MapToDto(Facility facility) => new()
|
||||
// --- Device binding (absorbed from PlaceService.AssignDevice, unchanged logic) ---
|
||||
|
||||
public async Task<object> AssignDevice(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
if (!data.ContainsKey("facilityId")) throw new KeyNotFoundException("facilityId is required");
|
||||
|
||||
var facilityId = Guid.Parse(data["facilityId"].GetString()!);
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
|
||||
var facility = await _facilityRepository.GetFacilityById(facilityId);
|
||||
if (facility == null || facility.FKClientId != clientId)
|
||||
throw new KeyNotFoundException("Facility not found");
|
||||
|
||||
var hasDeviceId = data.ContainsKey("deviceId") && data["deviceId"].ValueKind != JsonValueKind.Null;
|
||||
if (!hasDeviceId)
|
||||
{
|
||||
await _facilityRepository.UnassignDeviceAsync(facilityId);
|
||||
}
|
||||
else
|
||||
{
|
||||
var deviceId = Guid.Parse(data["deviceId"].GetString()!);
|
||||
var device = await _facilityRepository.GetDeviceByIdForClientAsync(deviceId, clientId)
|
||||
?? throw new KeyNotFoundException("Device not found");
|
||||
|
||||
if (await _facilityRepository.IsDeviceAssignedToAnotherFacilityAsync(deviceId, facilityId))
|
||||
throw new ArgumentException("This device is already assigned to another facility.");
|
||||
|
||||
await _facilityRepository.AssignDeviceAsync(facilityId, deviceId);
|
||||
}
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
var devicesByFacility = await _facilityRepository.GetDevicesByFacilityIdsAsync(new List<Guid> { facilityId });
|
||||
return MapToDto(facility, devicesByFacility.GetValueOrDefault(facilityId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Availability (new) ---
|
||||
|
||||
public async Task<object> GetAvailability(object payload, HttpContext httpContext)
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
if (!data.ContainsKey("startTime")) throw new KeyNotFoundException("startTime is required");
|
||||
if (!data.ContainsKey("durationMinutes")) throw new KeyNotFoundException("durationMinutes is required");
|
||||
|
||||
var startTime = DateTime.Parse(data["startTime"].GetString()!).ToUniversalTime();
|
||||
var durationMinutes = data["durationMinutes"].GetInt32();
|
||||
if (durationMinutes < 1) throw new ArgumentException("durationMinutes must be at least 1");
|
||||
var endTime = startTime.AddMinutes(durationMinutes);
|
||||
|
||||
Guid? facilityId = data.ContainsKey("facilityId") && data["facilityId"].ValueKind != JsonValueKind.Null
|
||||
? Guid.Parse(data["facilityId"].GetString()!)
|
||||
: null;
|
||||
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
var facilities = facilityId.HasValue
|
||||
? new List<Facility> { await _facilityRepository.GetFacilityById(facilityId.Value) is { } f && f.FKClientId == clientId ? f : throw new KeyNotFoundException("Facility not found") }
|
||||
: await _facilityRepository.GetAllFacilities(clientId, includeInactive: false);
|
||||
|
||||
var result = new List<FacilityAvailabilityDto>();
|
||||
foreach (var facility in facilities)
|
||||
{
|
||||
if (facility.BookingMode == "AdHoc")
|
||||
{
|
||||
var overlapping = await _facilityRepository.GetOverlappingAdhocBookingCountAsync(facility.FacilityId, startTime, endTime);
|
||||
result.Add(new FacilityAvailabilityDto
|
||||
{
|
||||
FacilityId = facility.FacilityId,
|
||||
FacilityName = facility.Name,
|
||||
BookingMode = facility.BookingMode,
|
||||
TotalUnits = facility.NumberOfUnits,
|
||||
UnitsFree = Math.Max(0, facility.NumberOfUnits - overlapping),
|
||||
PricePerHour = facility.PricePerHour
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var sessions = await _facilityRepository.GetSessions(clientId, facility.FacilityId, startTime, endTime);
|
||||
var overlappingSessions = sessions.Where(s => s.StartTime < endTime && s.EndTime > startTime).ToList();
|
||||
var totalCapacity = overlappingSessions.Sum(s => s.Capacity ?? facility.DefaultCapacity);
|
||||
var totalBooked = 0;
|
||||
foreach (var s in overlappingSessions)
|
||||
totalBooked += await _facilityRepository.GetActiveBookingCount(s.FacilitySessionId);
|
||||
|
||||
result.Add(new FacilityAvailabilityDto
|
||||
{
|
||||
FacilityId = facility.FacilityId,
|
||||
FacilityName = facility.Name,
|
||||
BookingMode = facility.BookingMode,
|
||||
TotalUnits = totalCapacity,
|
||||
UnitsFree = Math.Max(0, totalCapacity - totalBooked),
|
||||
PricePerHour = facility.PricePerHour
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static FacilityDto MapToDto(Facility facility, AccessDevice? device) => new()
|
||||
{
|
||||
FacilityId = facility.FacilityId,
|
||||
Name = facility.Name,
|
||||
FacilityType = facility.FacilityType,
|
||||
BookingMode = facility.BookingMode,
|
||||
Description = facility.Description,
|
||||
DefaultCapacity = facility.DefaultCapacity,
|
||||
NumberOfUnits = facility.NumberOfUnits,
|
||||
PricePerHour = facility.PricePerHour,
|
||||
IsActive = facility.IsActive,
|
||||
CreatedAt = facility.CreatedAt
|
||||
CreatedAt = facility.CreatedAt,
|
||||
DeviceId = device?.DeviceId,
|
||||
DeviceName = device?.DeviceName
|
||||
};
|
||||
|
||||
private static FacilitySessionDto MapToDto(FacilitySession session, string? facilityName = null, int bookedCount = 0) => new()
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
using Fitness_core_BE.DTOs.Place;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Infra.UoW;
|
||||
using Fitness_core_BE.Interfaces;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Fitness_core_BE.Services
|
||||
{
|
||||
public class PlaceService : IPlaceService
|
||||
{
|
||||
private readonly IPlaceRepo _placeRepo;
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly UserRoleHelper _userRoleHelper;
|
||||
|
||||
public PlaceService(IPlaceRepo placeRepo, IUnitOfWork uow, UserRoleHelper userRoleHelper)
|
||||
{
|
||||
_placeRepo = placeRepo;
|
||||
_uow = uow;
|
||||
_userRoleHelper = userRoleHelper;
|
||||
}
|
||||
|
||||
public async Task<object> GetAllPlaces(object payload, HttpContext httpContext)
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
bool includeInactive = data.ContainsKey("includeInactive") && data["includeInactive"].GetBoolean();
|
||||
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
var places = await _placeRepo.GetAllPlaces(clientId, includeInactive);
|
||||
|
||||
var devicesByPlace = await _placeRepo.GetDevicesByPlaceIdsAsync(places.Select(p => p.PlaceId).ToList());
|
||||
|
||||
return places.Select(p => MapToDto(p, devicesByPlace.GetValueOrDefault(p.PlaceId))).ToList();
|
||||
}
|
||||
|
||||
public async Task<object> CreatePlace(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
if (!data.ContainsKey("name")) throw new KeyNotFoundException("name is required");
|
||||
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
|
||||
var place = new Place
|
||||
{
|
||||
FKClientId = clientId,
|
||||
Name = data["name"].GetString() ?? "",
|
||||
Description = data.ContainsKey("description") ? data["description"].GetString() : null
|
||||
};
|
||||
|
||||
var result = await _placeRepo.CreatePlace(place);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return MapToDto(result, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> UpdatePlace(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
if (!data.ContainsKey("placeId")) throw new KeyNotFoundException("placeId is required");
|
||||
|
||||
var placeId = Guid.Parse(data["placeId"].GetString()!);
|
||||
var existing = await _placeRepo.GetPlaceById(placeId)
|
||||
?? throw new KeyNotFoundException("Place not found");
|
||||
|
||||
if (data.ContainsKey("name")) existing.Name = data["name"].GetString() ?? existing.Name;
|
||||
if (data.ContainsKey("description")) existing.Description = data["description"].GetString();
|
||||
if (data.ContainsKey("isActive")) existing.IsActive = data["isActive"].GetBoolean();
|
||||
|
||||
var result = await _placeRepo.UpdatePlace(existing);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
var devicesByPlace = await _placeRepo.GetDevicesByPlaceIdsAsync(new List<Guid> { result.PlaceId });
|
||||
return MapToDto(result, devicesByPlace.GetValueOrDefault(result.PlaceId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> SetPlaceStatus(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
var placeId = Guid.Parse(data["placeId"].GetString()!);
|
||||
var isActive = data["isActive"].GetBoolean();
|
||||
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
var place = await _placeRepo.SetPlaceActive(placeId, clientId, isActive)
|
||||
?? throw new KeyNotFoundException("Place not found");
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
var devicesByPlace = await _placeRepo.GetDevicesByPlaceIdsAsync(new List<Guid> { place.PlaceId });
|
||||
return MapToDto(place, devicesByPlace.GetValueOrDefault(place.PlaceId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> AssignDevice(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
if (!data.ContainsKey("placeId")) throw new KeyNotFoundException("placeId is required");
|
||||
|
||||
var placeId = Guid.Parse(data["placeId"].GetString()!);
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
|
||||
var place = await _placeRepo.GetPlaceById(placeId);
|
||||
if (place == null || place.FKClientId != clientId)
|
||||
throw new KeyNotFoundException("Place not found");
|
||||
|
||||
var hasDeviceId = data.ContainsKey("deviceId") && data["deviceId"].ValueKind != JsonValueKind.Null;
|
||||
if (!hasDeviceId)
|
||||
{
|
||||
await _placeRepo.UnassignDeviceAsync(placeId);
|
||||
}
|
||||
else
|
||||
{
|
||||
var deviceId = Guid.Parse(data["deviceId"].GetString()!);
|
||||
var device = await _placeRepo.GetDeviceByIdForClientAsync(deviceId, clientId)
|
||||
?? throw new KeyNotFoundException("Device not found");
|
||||
|
||||
if (await _placeRepo.IsDeviceAssignedToAnotherPlaceAsync(deviceId, placeId))
|
||||
throw new ArgumentException("This device is already assigned to another place.");
|
||||
|
||||
await _placeRepo.AssignDeviceAsync(placeId, deviceId);
|
||||
}
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
var devicesByPlace = await _placeRepo.GetDevicesByPlaceIdsAsync(new List<Guid> { placeId });
|
||||
return MapToDto(place, devicesByPlace.GetValueOrDefault(placeId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static PlaceDto MapToDto(Place place, AccessDevice? device) => new()
|
||||
{
|
||||
PlaceId = place.PlaceId,
|
||||
Name = place.Name,
|
||||
Description = place.Description,
|
||||
IsActive = place.IsActive,
|
||||
CreatedAt = place.CreatedAt,
|
||||
DeviceId = device?.DeviceId,
|
||||
DeviceName = device?.DeviceName
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using Fitness_core_BE.DTOs.RfidTag;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Infra.UoW;
|
||||
using Fitness_core_BE.Interfaces;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models;
|
||||
|
||||
namespace Fitness_core_BE.Services
|
||||
{
|
||||
public class RfidTagService(IRfidTagRepository rfidTagRepository, IUnitOfWork uow, UserRoleHelper userRoleHelper) : IRfidTagService
|
||||
{
|
||||
private static readonly string[] StaffSettableStatuses = ["Available", "Lost", "Retired"];
|
||||
|
||||
private readonly IRfidTagRepository _rfidTagRepository = rfidTagRepository;
|
||||
private readonly IUnitOfWork _uow = uow;
|
||||
private readonly UserRoleHelper _userRoleHelper = userRoleHelper;
|
||||
|
||||
public async Task<object> GetAllTags(object payload, HttpContext httpContext)
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
string? status = data.ContainsKey("status") ? data["status"].GetString() : null;
|
||||
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
var tags = await _rfidTagRepository.GetAllTags(clientId, status);
|
||||
|
||||
return tags.Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<object> CreateTag(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
if (!data.ContainsKey("tagCode")) throw new KeyNotFoundException("tagCode is required");
|
||||
|
||||
var tagCode = data["tagCode"].GetString() ?? throw new ArgumentException("tagCode is required");
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
|
||||
if (await _rfidTagRepository.TagCodeExistsAsync(clientId, tagCode))
|
||||
throw new ArgumentException("A tag with this code already exists.");
|
||||
|
||||
var tag = new RfidTag
|
||||
{
|
||||
FKClientId = clientId,
|
||||
TagCode = tagCode,
|
||||
Label = data.ContainsKey("label") ? data["label"].GetString() : null
|
||||
};
|
||||
|
||||
var result = await _rfidTagRepository.CreateTag(tag);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return MapToDto(result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> UpdateTag(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
if (!data.ContainsKey("tagId")) throw new KeyNotFoundException("tagId is required");
|
||||
|
||||
var tagId = Guid.Parse(data["tagId"].GetString()!);
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
|
||||
var existing = await _rfidTagRepository.GetTagById(tagId, clientId)
|
||||
?? throw new KeyNotFoundException("RFID tag not found");
|
||||
|
||||
if (data.ContainsKey("tagCode"))
|
||||
{
|
||||
var tagCode = data["tagCode"].GetString() ?? existing.TagCode;
|
||||
if (await _rfidTagRepository.TagCodeExistsAsync(clientId, tagCode, tagId))
|
||||
throw new ArgumentException("A tag with this code already exists.");
|
||||
existing.TagCode = tagCode;
|
||||
}
|
||||
if (data.ContainsKey("label")) existing.Label = data["label"].GetString();
|
||||
|
||||
var result = await _rfidTagRepository.UpdateTag(existing);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return MapToDto(result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> SetTagStatus(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
var tagId = Guid.Parse(data["tagId"].GetString()!);
|
||||
var status = data["status"].GetString() ?? throw new ArgumentException("status is required");
|
||||
|
||||
if (!StaffSettableStatuses.Contains(status))
|
||||
throw new ArgumentException($"status must be one of: {string.Join(", ", StaffSettableStatuses)}");
|
||||
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||
var tag = await _rfidTagRepository.SetStatusAsync(tagId, clientId, status)
|
||||
?? throw new KeyNotFoundException("RFID tag not found");
|
||||
|
||||
await _uow.CommitAsync();
|
||||
return MapToDto(tag);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static RfidTagDto MapToDto(RfidTag tag) => new()
|
||||
{
|
||||
RfidTagId = tag.RfidTagId,
|
||||
TagCode = tag.TagCode,
|
||||
Label = tag.Label,
|
||||
Status = tag.Status,
|
||||
CreatedAt = tag.CreatedAt,
|
||||
UpdatedAt = tag.UpdatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user