Files
Core_Be/Models/CustomerPlan.cs
HarithaRandunu 5d711bd13e Facility booking-mode redesign, multi-plan subscriptions, and Classes
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>
2026-10-09 14:26:59 +05:30

77 lines
3.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}