5d711bd13e
Implements the facility-classes-booking-dashboard plan (backend): - Facility.BookingMode (single Scheduled/AdHoc choice) replaced with independent SupportsClasses/SupportsBooking flags, so one facility can run a class and accept walk-in bookings; a running class now blocks walk-in booking for its window on both create and reschedule. - CustomerPlan.GrantsGymAccess + CustomerPlanClass join table let a customer hold one gym-access plan plus any number of class-only plans. Replaced the old single-subscription resolution (GetActiveSubscriptionByCustomerId/HasAnyActiveSubscriptionAsync) with gym-access-aware and class-aware resolvers across booking creation/cancellation, plan assignment, and customer self-service. Fixed ChangeMembershipPlan/renewal plan-changes, which resolved "the subscription to replace" by most-recent-created and would have deactivated a member's class subscription instead of their gym one. - New FacilityClass model: recurring class definitions (Daily/Weekly/ Monthly/Custom) with lazy occurrence generation (8-week horizon, race-safe via a unique index) and proper unlimited-capacity handling. "Mark not held" replaces the old bare-cancel: cascades to cancel and refund every confirmed booking on the occurrence through the same core CancelBooking uses, and frees the slot for walk-in booking. Scheduling conflicts (a walk-in booking now overlapping a class) are surfaced for staff to resolve via the existing Reschedule flow. - Renamed the pre-existing "AdHoc" booking-mode terminology to "WalkIn" throughout (routes, methods, DTOs, the stored BookingMode value) for a clearer, simpler name. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
77 lines
3.9 KiB
C#
77 lines
3.9 KiB
C#
using System.ComponentModel.DataAnnotations;
|
||
using System.ComponentModel.DataAnnotations.Schema;
|
||
|
||
namespace Fitness_core_BE.Models
|
||
{
|
||
public class CustomerPlan
|
||
{
|
||
[Key]
|
||
public Guid CustomerPlanId { get; set; }
|
||
public Guid FKClientId { get; set; }
|
||
[ForeignKey("FKClientId")]
|
||
public virtual Client Client { get; set; }
|
||
public string? PlanName { get; set; }
|
||
public string? PlanDescription { get; set; }
|
||
public decimal PlanPrice { get; set; }
|
||
|
||
/// <summary>Whether this plan charges a one-time fee when a new subscription starts.</summary>
|
||
public bool HasOnboardingFee { get; set; } = false;
|
||
|
||
/// <summary>One-time amount added to the first invoice/installment.</summary>
|
||
public decimal OnboardingFeeAmount { get; set; } = 0m;
|
||
|
||
/// <summary>Used when DurationMode == "Rolling"; 0 for FixedRange plans.</summary>
|
||
public int DurationInDays { get; set; }
|
||
|
||
/// <summary>"Individual" | "Group" (couple/family — one subscription covers MemberLimit members).</summary>
|
||
public string PlanScope { get; set; } = "Individual";
|
||
|
||
/// <summary>1 for Individual; >= 2 for Group (couple = 2).</summary>
|
||
public int MemberLimit { get; set; } = 1;
|
||
|
||
/// <summary>"Rolling" (DurationInDays from join date) | "FixedRange" (ValidFrom–ValidTo).</summary>
|
||
public string DurationMode { get; set; } = "Rolling";
|
||
|
||
public DateTime? ValidFrom { get; set; }
|
||
public DateTime? ValidTo { get; set; }
|
||
|
||
/// <summary>Owner decides: when true the price splits into InstallmentCount scheduled parts.</summary>
|
||
public bool AllowSplitPayments { get; set; } = false;
|
||
|
||
/// <summary>Number of mostly-equal installments (1 = full payment upfront).</summary>
|
||
public int InstallmentCount { get; set; } = 1;
|
||
|
||
/// <summary>Null = unlimited-access plan (existing gym behavior). A number = this plan
|
||
/// grants that many bookable Facility sessions per billing cycle (e.g. "4 pool sessions
|
||
/// a month") — enforced by the booking flow, not by this model.</summary>
|
||
public int? SessionsPerCycle { get; set; }
|
||
|
||
/// <summary>"NoRollover" (unused sessions are forfeited when the cycle renews) |
|
||
/// "GracePeriod" (unused sessions stay usable for RolloverGraceDays after renewal).
|
||
/// Set per package at creation time, per owner's requirement.</summary>
|
||
public string SessionRolloverPolicy { get; set; } = "NoRollover";
|
||
|
||
/// <summary>Days into the new cycle that last cycle's unused sessions remain usable.
|
||
/// Only meaningful when SessionRolloverPolicy == "GracePeriod".</summary>
|
||
public int RolloverGraceDays { get; set; } = 0;
|
||
|
||
/// <summary>"Prepaid" (pay before the cycle starts, existing behavior) | "Postpaid"
|
||
/// (use first, invoice at cycle end — reuses Client.LockAfterDaysOverdue /
|
||
/// OverdueLockJob to suspend access if that invoice goes unpaid past due). Asked once,
|
||
/// per package, at creation time per owner's requirement.</summary>
|
||
public string BillingTiming { get; set; } = "Prepaid";
|
||
|
||
public bool IsActive { get; set; } = true;
|
||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||
|
||
/// <summary>Meaningful only for a plan linked to one or more classes (via
|
||
/// CustomerPlanClass) -- never consulted for an ordinary plan with no class links,
|
||
/// which always grants gym access regardless of this flag's stored value. Lets a class
|
||
/// plan optionally also grant general gym-floor access ("class + gym" as one product)
|
||
/// instead of class-only. Migration default true (so every pre-existing plan, which has
|
||
/// no class links, keeps behaving exactly as before); new class plans default this to
|
||
/// false in the UI.</summary>
|
||
public bool GrantsGymAccess { get; set; } = true;
|
||
}
|
||
}
|