using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Fitness_core_BE.Models { /// One reservation against a Facility — either a Scheduled-mode booking against a /// pre-created FacilitySession (many bookings share one capacity pool), or an AdHoc-mode /// booking that claims one of the Facility's NumberOfUnits directly for an arbitrary time /// window, no session needed. Either an existing member using a CustomerPlan session /// allowance (FKCustomerId set, IsGuest = false) or a non-member who booked online and pays /// per-session (IsGuest = true, no membership at all). The guest's identity proof for /// check-in is the OTP issued at booking time, verified at the door instead of a /// fingerprint/face profile a non-member will never have. public class Booking { [Key] public Guid BookingId { get; set; } /// Null for an AdHoc booking (no pre-created session). public Guid? FKFacilitySessionId { get; set; } [ForeignKey(nameof(FKFacilitySessionId))] public FacilitySession? FacilitySession { get; set; } /// Always set, for both modes -- copied from FacilitySession.FKFacilityId at /// creation (Scheduled) or the booked facility directly (AdHoc). Every place that needs /// "which facility / what time window" reads these (and StartTime/EndTime below) /// uniformly instead of reaching through FacilitySession, which doesn't exist for AdHoc /// bookings. public Guid FKFacilityId { get; set; } [ForeignKey(nameof(FKFacilityId))] public Facility Facility { get; set; } /// Copied from FacilitySession.StartTime (Scheduled) or the requested start /// (AdHoc) at creation; updated in place by Reschedule. public DateTime StartTime { get; set; } /// Same pattern as StartTime. public DateTime EndTime { get; set; } /// Informational headcount for an AdHoc booking (e.g. "how many will come") -- /// never enforced as a capacity mechanic; each AdHoc booking claims one whole unit /// regardless of party size. Null for Scheduled bookings. public int? PartySize { get; set; } /// The RFID tag handed to the party at check-in, picked from the club's managed /// tag inventory (RfidTag) -- never free-typed. Released back to "Available" by the /// Complete action or the no-show/completion sweep, whichever runs first (both use the /// same idempotent guarded update, so a race between them is a harmless no-op). public Guid? FKRfidTagId { get; set; } [ForeignKey(nameof(FKRfidTagId))] public RfidTag? RfidTag { get; set; } public DateTime? RfidAssignedAt { get; set; } public DateTime? RfidReturnedAt { get; set; } public Guid FKClientId { get; set; } [ForeignKey(nameof(FKClientId))] public Client Client { get; set; } public bool IsGuest { get; set; } = false; // Member path. Tenant-bucket entity: holds a TenantCustomerLinkId, not the central // Customer.CusId directly -- see Models/TenantCustomerLink.cs. public Guid? FKCustomerId { get; set; } [ForeignKey(nameof(FKCustomerId))] public TenantCustomerLink? TenantCustomerLink { get; set; } // A member's booking draws down this specific subscription's per-cycle session // allowance; null for guest bookings (they have no subscription at all). public Guid? FKCustomerSubscriptionId { get; set; } [ForeignKey(nameof(FKCustomerSubscriptionId))] public CustomerSubscription? CustomerSubscription { get; set; } // Guest path -- no member record exists at all, so identity is captured inline. [MaxLength(200)] public string? GuestName { get; set; } [MaxLength(32)] public string? GuestPhone { get; set; } /// Issued when the booking is created; the guest presents this at the door /// instead of a fingerprint/face profile. Members don't need one -- their existing /// access credential covers check-in. [MaxLength(10)] public string? OtpCode { get; set; } public DateTime? OtpVerifiedAt { get; set; } /// "Paid" | "Unpaid" | "NotRequired" (member using a plan's included session). public string PaymentStatus { get; set; } = "NotRequired"; public decimal AmountCharged { get; set; } = 0m; // Set only once payment is confirmed for a pay-per-session guest booking, or // immediately for a member session (no payment gate) -- see PaymentStatus. public Guid? FKPaymentTransactionId { get; set; } [ForeignKey(nameof(FKPaymentTransactionId))] public PaymentTransaction? PaymentTransaction { get; set; } /// "Confirmed" | "CheckedIn" | "Cancelled" | "NoShow" | "Completed". NoShow and /// Completed are reached either by explicit staff action (MarkNoShow/Complete) or lazily, /// swept by BookingService.SweepOverdueBookingsAsync at the top of every check-in call /// (not a background job -- overdue bookings are reconciled the next time any check-in /// happens for this client, across every facility). public string Status { get; set; } = "Confirmed"; public DateTime BookedAt { get; set; } = DateTime.UtcNow; public DateTime? CheckedInAt { get; set; } /// Set when the booking reaches Completed, whether via the manual Complete /// action or the lazy overdue-sweep -- the actual moment it was reconciled, not /// necessarily EndTime. public DateTime? CheckedOutAt { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } }