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; /// "Cash" | "Bank" -- set alongside PaymentStatus becoming "Paid" (MarkGuestBookingPaid /// or the Complete checkout flow). Null until then. [MaxLength(20)] public string? PaymentMethod { get; set; } /// Set the moment PaymentStatus becomes "Paid" -- distinct from BookedAt/CheckedOutAt, /// this is specifically when the money was recorded, used to bucket booking revenue into /// Profit & Loss and to date-stamp the BookingInvoice snapshot. public DateTime? PaidAt { get; set; } // 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 is /// reached either by explicit staff action (MarkNoShow) or lazily, swept by /// BookingService.SweepOverdueBookingsAsync at the top of every check-in or checkout call /// (not a background job -- overdue Confirmed bookings are reconciled the next time any /// check-in/checkout happens for this client, across every facility). Completed is /// reached ONLY by the explicit Complete action -- a guest booking can't reach it while /// still Unpaid, so there's no automatic path that would check someone out unpaid. public string Status { get; set; } = "Confirmed"; public DateTime BookedAt { get; set; } = DateTime.UtcNow; public DateTime? CheckedInAt { get; set; } /// Set when the booking reaches Completed via the manual Complete action -- the /// actual moment it was checked out, not necessarily EndTime. public DateTime? CheckedOutAt { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } }