Files
Core_Be/Services/CustomerPlanService.cs
T
Dhananjaya99 bffc9b1686 first commit
2026-09-28 11:44:26 +05:30

441 lines
20 KiB
C#

using Fitness_core_BE.Interfaces;
using Fitness_core_BE.Interfaces.Services;
using Fitness_core_BE.Models;
using Fitness_core_BE.Infra.UoW;
using Fitness_core_BE.DTOs.CustomerPlan;
using System.Text.Json;
using Fitness_core_BE.Helpers;
namespace Fitness_core_BE.Services.CustomerPlanService
{
public class CustomerPlanService : ICustomerPlanService
{
private const string ScopeIndividual = "Individual";
private const string ScopeGroup = "Group";
private const string ModeRolling = "Rolling";
private const string ModeFixedRange = "FixedRange";
private const string RolloverNone = "NoRollover";
private const string RolloverGrace = "GracePeriod";
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;
}
public async Task<object> GetAllCustomerPlans(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
bool includeInactive = data != null &&
data.ContainsKey("includeInactive") &&
data["includeInactive"].GetBoolean();
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid contextClientId = await _userRoleHelper.GetClientId(userId);
Guid? requestedClientId = null;
if (data != null && data.ContainsKey("FKClientId") && data["FKClientId"].ValueKind != JsonValueKind.Null)
{
requestedClientId = Guid.Parse(data["FKClientId"].GetString()!);
}
var clientId = requestedClientId ?? contextClientId;
var plans = await _customerPlanRepository.GetAllCustomerPlans(includeInactive, clientId);
var placeIdsByPlan = await _placeRepo.GetPlaceIdsByPlanIdsAsync(plans.Select(p => p.CustomerPlanId).ToList());
var allPlaceIds = placeIdsByPlan.Values.SelectMany(ids => ids).Distinct().ToList();
var placeNames = await _placeRepo.GetPlaceNamesByIdsAsync(allPlaceIds);
return plans.Select(p =>
{
var placeIds = placeIdsByPlan.GetValueOrDefault(p.CustomerPlanId) ?? new List<Guid>();
return MapToDto(p, p.Client?.ClientName, placeIds, placeNames);
}).ToList();
}
public async Task<object> GetCustomerPlanById(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
if (!data!.ContainsKey("customerPlanId"))
{
throw new KeyNotFoundException("customerPlanId is required");
}
var customerPlanId = Guid.Parse(data["customerPlanId"].GetString()!);
var plan = await _customerPlanRepository.GetCustomerPlanById(customerPlanId);
if (plan == null)
{
throw new KeyNotFoundException("Customer Plan not found");
}
return await MapToDtoWithPlacesAsync(plan, plan.Client?.ClientName);
}
public async Task<object> CreateCustomerPlan(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data!.ContainsKey("planName"))
{
throw new KeyNotFoundException("planName is required");
}
if (!data.ContainsKey("planPrice"))
{
throw new KeyNotFoundException("planPrice is required");
}
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid clientId = await _userRoleHelper.GetClientId(userId);
var customerPlan = new Models.CustomerPlan
{
FKClientId = clientId,
PlanName = data["planName"].GetString(),
PlanDescription = data.ContainsKey("planDescription")
? data["planDescription"].GetString()
: null,
PlanPrice = data["planPrice"].GetDecimal(),
HasOnboardingFee = data.ContainsKey("hasOnboardingFee") && data["hasOnboardingFee"].GetBoolean(),
OnboardingFeeAmount = data.ContainsKey("onboardingFeeAmount") ? data["onboardingFeeAmount"].GetDecimal() : 0m,
PlanScope = data.ContainsKey("planScope") ? data["planScope"].GetString() ?? ScopeIndividual : ScopeIndividual,
MemberLimit = data.ContainsKey("memberLimit") ? data["memberLimit"].GetInt32() : 1,
DurationMode = data.ContainsKey("durationMode") ? data["durationMode"].GetString() ?? ModeRolling : ModeRolling,
DurationInDays = data.ContainsKey("durationInDays") ? data["durationInDays"].GetInt32() : 0,
ValidFrom = DataExtractor.GetOptionalDateTime(data, "validFrom"),
ValidTo = DataExtractor.GetOptionalDateTime(data, "validTo"),
AllowSplitPayments = data.ContainsKey("allowSplitPayments") && data["allowSplitPayments"].GetBoolean(),
InstallmentCount = data.ContainsKey("installmentCount") ? data["installmentCount"].GetInt32() : 0,
SessionsPerCycle = data.ContainsKey("sessionsPerCycle") && data["sessionsPerCycle"].ValueKind != JsonValueKind.Null
? data["sessionsPerCycle"].GetInt32()
: null,
SessionRolloverPolicy = data.ContainsKey("sessionRolloverPolicy") ? data["sessionRolloverPolicy"].GetString() ?? RolloverNone : RolloverNone,
RolloverGraceDays = data.ContainsKey("rolloverGraceDays") ? data["rolloverGraceDays"].GetInt32() : 0,
BillingTiming = data.ContainsKey("billingTiming") ? data["billingTiming"].GetString() ?? BillingPrepaid : BillingPrepaid
};
NormalizeAndValidate(customerPlan);
var result = await _customerPlanRepository.CreateCustomerPlan(customerPlan);
await _uow.SaveChangesAsync();
var placeIds = DataExtractor.GetOptionalGuidList(data, "placeIds");
if (placeIds != null)
await _placeRepo.SetPlanPlacesAsync(result.CustomerPlanId, placeIds);
await _uow.CommitAsync();
// Fetch the created plan with client details
var createdPlan = await _customerPlanRepository.GetCustomerPlanById(result.CustomerPlanId);
return await MapToDtoWithPlacesAsync(result, createdPlan?.Client?.ClientName);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> UpdateCustomerPlan(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data!.ContainsKey("customerPlanId"))
{
throw new KeyNotFoundException("customerPlanId is required");
}
var customerPlanId = Guid.Parse(data["customerPlanId"].GetString()!);
var existingPlan = await _customerPlanRepository.GetCustomerPlanById(customerPlanId);
if (existingPlan == null)
{
throw new KeyNotFoundException("Customer Plan not found");
}
// Merge: start from existing values, override only the provided keys,
// then re-validate the combined configuration.
if (data.ContainsKey("planName")) existingPlan.PlanName = data["planName"].GetString();
if (data.ContainsKey("planDescription")) existingPlan.PlanDescription = data["planDescription"].GetString();
if (data.ContainsKey("planPrice")) existingPlan.PlanPrice = data["planPrice"].GetDecimal();
if (data.ContainsKey("hasOnboardingFee")) existingPlan.HasOnboardingFee = data["hasOnboardingFee"].GetBoolean();
if (data.ContainsKey("onboardingFeeAmount")) existingPlan.OnboardingFeeAmount = data["onboardingFeeAmount"].GetDecimal();
if (data.ContainsKey("planScope")) existingPlan.PlanScope = data["planScope"].GetString() ?? existingPlan.PlanScope;
if (data.ContainsKey("memberLimit")) existingPlan.MemberLimit = data["memberLimit"].GetInt32();
if (data.ContainsKey("durationMode")) existingPlan.DurationMode = data["durationMode"].GetString() ?? existingPlan.DurationMode;
if (data.ContainsKey("durationInDays")) existingPlan.DurationInDays = data["durationInDays"].GetInt32();
if (data.ContainsKey("validFrom")) existingPlan.ValidFrom = DataExtractor.GetOptionalDateTime(data, "validFrom");
if (data.ContainsKey("validTo")) existingPlan.ValidTo = DataExtractor.GetOptionalDateTime(data, "validTo");
if (data.ContainsKey("allowSplitPayments")) existingPlan.AllowSplitPayments = data["allowSplitPayments"].GetBoolean();
if (data.ContainsKey("installmentCount")) existingPlan.InstallmentCount = data["installmentCount"].GetInt32();
if (data.ContainsKey("sessionsPerCycle"))
existingPlan.SessionsPerCycle = data["sessionsPerCycle"].ValueKind == JsonValueKind.Null ? null : data["sessionsPerCycle"].GetInt32();
if (data.ContainsKey("sessionRolloverPolicy")) existingPlan.SessionRolloverPolicy = data["sessionRolloverPolicy"].GetString() ?? existingPlan.SessionRolloverPolicy;
if (data.ContainsKey("rolloverGraceDays")) existingPlan.RolloverGraceDays = data["rolloverGraceDays"].GetInt32();
if (data.ContainsKey("billingTiming")) existingPlan.BillingTiming = data["billingTiming"].GetString() ?? existingPlan.BillingTiming;
if (data.ContainsKey("isActive")) existingPlan.IsActive = data["isActive"].GetBoolean();
NormalizeAndValidate(existingPlan);
var result = await _customerPlanRepository.UpdateCustomerPlan(existingPlan);
var placeIds = DataExtractor.GetOptionalGuidList(data, "placeIds");
if (placeIds != null)
await _placeRepo.SetPlanPlacesAsync(result.CustomerPlanId, placeIds);
await _uow.CommitAsync();
var updatedPlan = await _customerPlanRepository.GetCustomerPlanById(result.CustomerPlanId);
return await MapToDtoWithPlacesAsync(result, updatedPlan?.Client?.ClientName);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> SetCustomerPlanStatus(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data!.ContainsKey("customerPlanId"))
{
throw new KeyNotFoundException("customerPlanId is required");
}
if (!data.ContainsKey("isActive"))
{
throw new KeyNotFoundException("isActive is required");
}
var customerPlanId = Guid.Parse(data["customerPlanId"].GetString()!);
var isActive = data["isActive"].GetBoolean();
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid clientId = await _userRoleHelper.GetClientId(userId);
// Scoped to the caller's gym, so a plan id from another gym reads as
// "not found" rather than being toggled.
var plan = await _customerPlanRepository.SetCustomerPlanActive(customerPlanId, clientId, isActive);
if (plan == null)
{
throw new KeyNotFoundException("Customer Plan not found");
}
await _uow.CommitAsync();
return await MapToDtoWithPlacesAsync(plan, plan.Client?.ClientName);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> DeleteCustomerPlan(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data!.ContainsKey("customerPlanId"))
{
throw new KeyNotFoundException("customerPlanId is required");
}
var customerPlanId = Guid.Parse(data["customerPlanId"].GetString()!);
// Check if hard delete is requested (default is soft delete)
bool softDelete = !data.ContainsKey("hardDelete") || !data["hardDelete"].GetBoolean();
var result = await _customerPlanRepository.DeleteCustomerPlan(customerPlanId, softDelete);
if (!result)
{
throw new KeyNotFoundException("Customer Plan not found");
}
await _uow.CommitAsync();
return new
{
success = true,
message = softDelete
? "Customer Plan deactivated successfully"
: "Customer Plan deleted successfully",
softDelete = softDelete
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
/// <summary>
/// Enforces the cross-field rules of the plan configuration and fills
/// derived defaults (member limit, installment count, cleared date range).
/// Throws ArgumentException on invalid combinations.
/// </summary>
private static void NormalizeAndValidate(Models.CustomerPlan plan)
{
// Scope
if (plan.PlanScope != ScopeIndividual && plan.PlanScope != ScopeGroup)
throw new ArgumentException($"planScope must be \"{ScopeIndividual}\" or \"{ScopeGroup}\"");
if (plan.PlanScope == ScopeGroup)
{
if (plan.MemberLimit < 2)
throw new ArgumentException("memberLimit must be at least 2 for Group plans (couple = 2)");
}
else
{
plan.MemberLimit = 1;
}
// Duration
if (plan.DurationMode != ModeRolling && plan.DurationMode != ModeFixedRange)
throw new ArgumentException($"durationMode must be \"{ModeRolling}\" or \"{ModeFixedRange}\"");
int effectiveDays;
if (plan.DurationMode == ModeRolling)
{
if (plan.DurationInDays < 1)
throw new ArgumentException("durationInDays (>= 1) is required for Rolling plans");
plan.ValidFrom = null;
plan.ValidTo = null;
effectiveDays = plan.DurationInDays;
}
else
{
if (plan.ValidFrom == null || plan.ValidTo == null)
throw new ArgumentException("validFrom and validTo are required for FixedRange plans");
if (plan.ValidTo <= plan.ValidFrom)
throw new ArgumentException("validTo must be after validFrom");
plan.DurationInDays = 0;
effectiveDays = (int)(plan.ValidTo.Value - plan.ValidFrom.Value).TotalDays;
}
if (plan.PlanPrice < 0)
throw new ArgumentException("planPrice must be a positive value");
if (plan.OnboardingFeeAmount < 0)
throw new ArgumentException("onboardingFeeAmount must be zero or greater");
if (!plan.HasOnboardingFee)
plan.OnboardingFeeAmount = 0m;
else if (plan.OnboardingFeeAmount <= 0)
throw new ArgumentException("onboardingFeeAmount must be greater than zero when hasOnboardingFee is true");
// Split payments: mostly-equal installments (e.g. Rs. 6000 / 6 months -> 6 x Rs. 1000).
if (plan.AllowSplitPayments)
{
if (plan.InstallmentCount < 2)
{
// Default: one installment per month of the plan's duration.
plan.InstallmentCount = Math.Max(2, (int)Math.Round(effectiveDays / 30.0));
}
}
else
{
plan.InstallmentCount = 1;
}
// Session-count / rollover / billing timing (new SRS requirements).
if (plan.SessionRolloverPolicy != RolloverNone && plan.SessionRolloverPolicy != RolloverGrace)
throw new ArgumentException($"sessionRolloverPolicy must be \"{RolloverNone}\" or \"{RolloverGrace}\"");
if (plan.SessionsPerCycle.HasValue)
{
if (plan.SessionsPerCycle.Value < 1)
throw new ArgumentException("sessionsPerCycle must be at least 1 when set");
if (plan.SessionRolloverPolicy == RolloverGrace && plan.RolloverGraceDays < 1)
throw new ArgumentException("rolloverGraceDays must be at least 1 when sessionRolloverPolicy is \"GracePeriod\"");
}
else
{
// No session allowance on this plan (unlimited-access, existing gym behavior) --
// rollover settings are meaningless without a per-cycle count to roll over.
plan.SessionRolloverPolicy = RolloverNone;
plan.RolloverGraceDays = 0;
}
if (plan.BillingTiming != BillingPrepaid && plan.BillingTiming != BillingPostpaid)
throw new ArgumentException($"billingTiming must be \"{BillingPrepaid}\" or \"{BillingPostpaid}\"");
}
private async Task<CustomerPlanDto> MapToDtoWithPlacesAsync(Models.CustomerPlan plan, string? clientName)
{
var placeIds = await _placeRepo.GetPlaceIdsForPlanAsync(plan.CustomerPlanId);
var placeNames = await _placeRepo.GetPlaceNamesByIdsAsync(placeIds);
return MapToDto(plan, clientName, placeIds, placeNames);
}
private static CustomerPlanDto MapToDto(
Models.CustomerPlan plan,
string? clientName,
List<Guid> placeIds,
Dictionary<Guid, string> placeNames) => new()
{
CustomerPlanId = plan.CustomerPlanId,
FKClientId = plan.FKClientId,
ClientName = clientName,
PlanName = plan.PlanName,
PlanDescription = plan.PlanDescription,
PlanPrice = plan.PlanPrice,
HasOnboardingFee = plan.HasOnboardingFee,
OnboardingFeeAmount = plan.OnboardingFeeAmount,
DurationInDays = plan.DurationInDays,
PlanScope = plan.PlanScope,
MemberLimit = plan.MemberLimit,
DurationMode = plan.DurationMode,
ValidFrom = plan.ValidFrom,
ValidTo = plan.ValidTo,
AllowSplitPayments = plan.AllowSplitPayments,
InstallmentCount = plan.InstallmentCount,
SessionsPerCycle = plan.SessionsPerCycle,
SessionRolloverPolicy = plan.SessionRolloverPolicy,
RolloverGraceDays = plan.RolloverGraceDays,
BillingTiming = plan.BillingTiming,
IsActive = plan.IsActive,
CreatedAt = plan.CreatedAt,
PlaceIds = placeIds,
PlaceNames = placeIds.Select(id => placeNames.GetValueOrDefault(id, "")).Where(n => n != "").ToList()
};
}
}